From 0a499636f99e716ba0bf39d18986c0da43974993 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Mon, 25 Sep 2023 18:02:01 -0700 Subject: [PATCH] import and export --- clearversion.xml | 8 ++ cmd/backup.go | 85 ++++++++++++ cmd/backups.go | 57 -------- cmd/commit.go | 46 ++++--- cmd/compare.go | 19 +-- cmd/delete.go | 2 +- cmd/discard.go | 83 ++++++++---- cmd/export.go | 91 +++++++++++++ cmd/import.go | 99 ++++++++++++++ cmd/load.go | 24 +++- cmd/root.go | 18 ++- cmd/run.go | 15 ++ cmd/save.go | 23 +--- cmd/set.go | 62 ++++----- cmd/show.go | 12 +- cmd/sysinfo.go | 14 +- internal/{parser.go => ConfigToOutput.go} | 41 +++--- internal/{xmlcompare.go => DiffXML.go} | 128 ++++++++++++++++-- internal/EtreeToJSON.go | 42 ++++++ internal/EtreeToTTY.go | 124 +++++++++++++++++ internal/FocusEtree.go | 77 +++++++++++ internal/LoadXMLFile.go | 45 ++++++ internal/PatchXML.go | 85 ++++++++++++ internal/PrintDocument.go | 37 +++++ internal/{printloadsave.go => SaveXMLFile.go} | 57 ++++---- internal/checkos.go | 18 ++- internal/color_map.go | 46 +++++++ internal/eTreeRender.go | 127 ----------------- internal/executecmd.go | 66 +++------ internal/{ssh.go => getSSHClient.go} | 15 ++ internal/log.go | 48 +++---- internal/setflags.go | 19 +++ internal/sftpCmd.go | 68 ++++++++++ internal/sshAgent_unix.go | 15 ++ internal/sshAgent_windows.go | 15 ++ tt.xml | 13 ++ 36 files changed, 1286 insertions(+), 458 deletions(-) create mode 100644 clearversion.xml create mode 100644 cmd/backup.go delete mode 100644 cmd/backups.go create mode 100644 cmd/export.go create mode 100644 cmd/import.go rename internal/{parser.go => ConfigToOutput.go} (64%) rename internal/{xmlcompare.go => DiffXML.go} (63%) create mode 100644 internal/EtreeToJSON.go create mode 100644 internal/EtreeToTTY.go create mode 100644 internal/FocusEtree.go create mode 100644 internal/LoadXMLFile.go create mode 100644 internal/PatchXML.go create mode 100644 internal/PrintDocument.go rename internal/{printloadsave.go => SaveXMLFile.go} (52%) create mode 100644 internal/color_map.go delete mode 100644 internal/eTreeRender.go rename internal/{ssh.go => getSSHClient.go} (77%) create mode 100644 internal/sftpCmd.go create mode 100644 tt.xml diff --git a/clearversion.xml b/clearversion.xml new file mode 100644 index 0000000..b403ee2 --- /dev/null +++ b/clearversion.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/cmd/backup.go b/cmd/backup.go new file mode 100644 index 0000000..1512723 --- /dev/null +++ b/cmd/backup.go @@ -0,0 +1,85 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + "strings" + + "github.com/beevik/etree" + "github.com/mihakralj/opnsense/internal" + "github.com/spf13/cobra" +) + +// backupCmd represents the backup command +var backupCmd = &cobra.Command{ + Use: "backup", + Short: `List available backup configurations in '/conf/backup' directory`, + Long: `The 'backup' command provides functionalities for managing and viewing backup XML configurations within your OPNsense firewall system. You can list all backup configurations or get details about a specific one.`, + Example: ` show backup Lists all backup XML configurations. + show backup Show details of a specific backup XML configuration`, + Run: func(cmd *cobra.Command, args []string) { + + backupdir := "/conf/backup/" + path := "backups" + filename := "" + + if len(args) > 0 { + filename = strings.TrimPrefix(args[0], "/") + if !strings.HasSuffix(filename, ".xml") { + filename = filename + ".xml" + } + path = path + "/" + filename + } + + internal.Checkos() + rootdoc := etree.NewDocument() + + bash := fmt.Sprintf(`echo -n '' && echo -n '' | sed 's/##/"/g'`, backupdir) + bash = bash + fmt.Sprintf(` && find %s -type f -exec sh -c 'echo $(stat -f "%%m" "$1") $(basename "$1") $(stat -f "%%z" "$1") $(md5sum "$1")' sh {} \; | sort -nr -k1`, backupdir) + bash = bash + `| awk '{ date = strftime("%Y-%m-%dT%H:%M:%S", $1); delta = systime() - $1; days = int(delta / 86400); hours = int((delta % 86400) / 3600); minutes = int((delta % 3600) / 60); seconds = int(delta % 60); age = days "d " hours "h " minutes "m " seconds "s"; print " <" $2 " age=\"" age "\">" date "" $3 "" $4 ""; } END { print ""; }'` + + backups := internal.ExecuteCmd(bash, host) + err := rootdoc.ReadFromString(backups) + if err != nil { + internal.Log(1, "format is not XML") + } + if len(args) > 0 { + internal.FullDepth() + + configdoc := internal.LoadXMLFile(configfile, host, false) + backupdoc := internal.LoadXMLFile(backupdir+filename, host, false) + + deltadoc := internal.DiffXML(backupdoc, configdoc, false) + + + // append all differences to the rootdoc + diffEl := rootdoc.FindElement(path).CreateElement("diff") + for _, child := range deltadoc.Root().ChildElements() { + diffEl.AddChild(child.Copy()) + } + + } + fmt.Println() + internal.PrintDocument(rootdoc, path) + + }, +} + +func init() { + backupCmd.Flags().IntVarP(&depth, "depth", "d", 1, "Specifies number of depth levels of returned tree (default: 1)") + rootCmd.AddCommand(backupCmd) +} diff --git a/cmd/backups.go b/cmd/backups.go deleted file mode 100644 index 86ea8e4..0000000 --- a/cmd/backups.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright © 2023 NAME HERE -*/ -package cmd - -import ( - "fmt" - - "github.com/beevik/etree" - "github.com/mihakralj/opnsense/internal" - "github.com/spf13/cobra" -) - -// backupCmd represents the backup command -var backupsCmd = &cobra.Command{ - Use: "backups", - Short: `List available backup configurations in '/conf/backup' directory`, - Long: `The 'backups' command provides functionalities for managing and viewing backup XML configurations within your OPNsense firewall system. You can list all backup configurations or get details about a specific one.`, - Example: ` show backup Lists all backup XML configurations. - show backup Show details of a specific backup XML configuration`, - Run: func(cmd *cobra.Command, args []string) { - - backupdir := "/conf/backup/" - path := "backups" - internal.Checkos() - backupdoc := etree.NewDocument() - bash := fmt.Sprintf(`echo -n "\n' | sed 's/##/"/g'`, backupdir) - bash = bash + fmt.Sprintf(`&&find %s -type f -exec sh -c 'echo $(stat -f "%%m" "$1") $(basename "$1") $(stat -f "%%z" "$1") $(md5sum "$1")' sh {} \; | sort -nr -k1`, backupdir) - bash = bash + `| awk '{ date = strftime("%Y-%m-%dT%H:%M:%S", $1); delta = systime() - $1; days = int(delta / 86400); hours = int((delta % 86400) / 3600); minutes = int((delta % 3600) / 60); seconds = int(delta % 60); age = days "d " hours "h " minutes "m " seconds "s"; print " <" $2 " age=\"" age "\">" date "" $3 "" $4 ""; } END { print ""; }'` - - backups := internal.ExecuteCmd(bash, host) - - err := backupdoc.ReadFromString(backups) - if err != nil { - internal.Log(1, "did not receive XML") - } - - backupout := "" - if xmlFlag { - backupout = internal.ConfigToXML(backupdoc, path) - } else if jsonFlag { - backupout = internal.ConfigToJSON(backupdoc, path) - } else if yamlFlag { - backupout = internal.ConfigToJSON(backupdoc, path) - } else { - backupout = internal.ConfigToTTY(backupdoc, path) - } - - fmt.Println(backupout) - - }, -} - -func init() { - backupsCmd.Flags().IntVarP(&depth, "depth", "d", 1, "Specify the depth of shown hierarchy (Default: 1)") - rootCmd.AddCommand(backupsCmd) -} diff --git a/cmd/commit.go b/cmd/commit.go index 74ff468..62a00a5 100644 --- a/cmd/commit.go +++ b/cmd/commit.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 MihaK mihak09@gmail.com +Copyright © 2023 Miha miha.kralj@outlook.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -30,36 +30,42 @@ var commitCmd = &cobra.Command{ Long: `The 'commit' command finalizes the staged changes made to the 'staging.xml' file, making them the active configuration for the OPNsense firewall system. This operation is the last step in a sequence that typically involves the 'set' and optionally 'discard' commands. The 'commit' action creates a backup of the active 'config.xml', moves 'staging.xml' to 'config.xml', and reloads the 'configd' service. `, -Example: ` opnsense commit Commit the changes in 'staging.xml' to become the active 'config.xml' + Example: ` opnsense commit Commit the changes in 'staging.xml' to become the active 'config.xml' opnsense commit --force Commit the changes without requiring interactive confirmation.`, Run: func(cmd *cobra.Command, args []string) { // check if staging.xml exists internal.Checkos() - bash := `if [ -f "` + stagingfile + `" ]; then echo "exists"; fi` + bash := `test -f "` + stagingfile + `" && echo "exists" || echo "missing"` fileexists := internal.ExecuteCmd(bash, host) if strings.TrimSpace(fileexists) != "exists" { - internal.Log(1, "no staging.xml detected - nothing to commit.") + fmt.Println("no staging.xml detected - nothing to commit.") + return } - internal.Log(2,"modifying "+configfile) + bash = `diff -q "` + configfile + `" "` + stagingfile + `" >& /dev/null && echo "same" || echo "diff"` + filesame := internal.ExecuteCmd(bash, host) + if strings.TrimSpace(filesame) != "diff" { + fmt.Println("staging.xml and config.xml are the same - nothing to commit.") + } + + configdoc := internal.LoadXMLFile(configfile, host, false) + stagingdoc := internal.LoadXMLFile(stagingfile, host, false) + + deltadoc := internal.DiffXML(configdoc, stagingdoc, false) + fmt.Println("\nChanges to be commited:") + internal.PrintDocument(deltadoc, "opnsense") + + internal.Log(2, "commiting %s to %s", stagingfile, configfile) // copy config.xml to /conf/backup dir - backupname := generateBackupFilename() - bash = `sudo cp -f ` + configfile + ` /conf/backup/` + backupname + ` && sudo mv -f /conf/staging.xml `+configfile - //internal.ExecuteCmd(bash, host) - fmt.Println(bash) - - bash = `if [ -f "` + configfile + `" ]; then echo "ok"; else echo "error"; fi` - fileexists = internal.ExecuteCmd(bash, host) - if fileexists == "ok" { - bash = `` - } else { - //error - bash = `` - } - // config reload - full or partial? + backupname := internal.GenerateBackupFilename() + bash = `sudo cp -f ` + configfile + ` /conf/backup/` + backupname + ` && sudo mv -f /conf/staging.xml ` + configfile + internal.ExecuteCmd(bash, host) + + fmt.Println("time to reload OPNSense!") + + //TODO: run php /usr/local/etc/rc.reload_all - fmt.Println(fileexists) }, } diff --git a/cmd/compare.go b/cmd/compare.go index 1cc2b38..e3ad866 100644 --- a/cmd/compare.go +++ b/cmd/compare.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 MihaK mihak09@gmail.com +Copyright © 2023 Miha miha.kralj@outlook.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,11 +22,13 @@ import ( "github.com/spf13/cobra" ) +var compact bool + // compareCmd represents the compare command var compareCmd = &cobra.Command{ Use: "compare [] []", Short: `Compare differences between two XML configuration files`, - Long: `The 'compare' command identifies differences between two XML configuration files for the OPNsense firewall system. When only one filename is provided, it shows the differences between that file and the current 'config.xml'. When no filenames are provided, it compares the current 'config.xml' with 'staging.xml', akin to the 'show' command.`, + Long: `The 'compare' command identifies differences between two XML configuration files for the OPNsense firewall system. When only one filename is provided, it shows the differences between that file and the current 'config.xml'. When no filenames are provided, it compares the current 'config.xml' with 'staging.xml', akin to the 'show' command.`, Example: ` opnsense compare b1.xml b2.xml Compare differences from 'b1.xml' to 'b2.xml' opnsense compare backup.xml Compare differences from 'backup.xml' to 'config.xml' opnsense compare Compare differences from 'config.xml' to 'staging.xml'`, @@ -71,21 +73,20 @@ var compareCmd = &cobra.Command{ } internal.Checkos() - olddoc := internal.LoadXMLFile(oldconfig, host) - if olddoc == nil { - internal.Log(1,"failed to get data from %s",oldconfig) - } - newdoc := internal.LoadXMLFile(newconfig, host) + olddoc := internal.LoadXMLFile(oldconfig, host, false) + newdoc := internal.LoadXMLFile(newconfig, host, true) if newdoc == nil { newdoc = olddoc } - deltadoc := internal.DiffXML(olddoc, newdoc, true) + + deltadoc := internal.DiffXML(olddoc, newdoc, !compact) internal.PrintDocument(deltadoc, path) }, } func init() { - compareCmd.Flags().IntVarP(&depth, "depth", "d", 1, "Specifies number of levels of returned tree (1-5)") + compareCmd.Flags().IntVarP(&depth, "depth", "d", 1, "Specifies number of depth levels of returned tree (default: 1)") + compareCmd.Flags().BoolVarP(&compact, "compact", "c", false, "Show only the net changes between configurations") rootCmd.AddCommand(compareCmd) } diff --git a/cmd/delete.go b/cmd/delete.go index f87d6d5..1b1c3b0 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 MihaK mihak09@gmail.com +Copyright © 2023 Miha miha.kralj@outlook.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/cmd/discard.go b/cmd/discard.go index 477a8fd..a59535d 100644 --- a/cmd/discard.go +++ b/cmd/discard.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 MihaK mihak09@gmail.com +Copyright © 2023 Miha miha.kralj@outlook.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -39,11 +39,8 @@ Use the 'discard' command cautiously to avoid losing uncommitted changes.`, internal.Checkos() - configdoc := internal.LoadXMLFile(configfile, host) - if configdoc == nil { - internal.Log(1, "failed to get data from %s", configfile) - } - stagingdoc := internal.LoadXMLFile(stagingfile, host) + configdoc := internal.LoadXMLFile(configfile, host, false) + stagingdoc := internal.LoadXMLFile(stagingfile, host, true) if stagingdoc == nil { stagingdoc = configdoc } @@ -53,36 +50,72 @@ Use the 'discard' command cautiously to avoid losing uncommitted changes.`, internal.Log(2, "Discarding all staged configuration changes.") stagingdoc = configdoc } else { - trimmedArg := strings.Trim(args[0], "/") - if matched, _ := regexp.MatchString(`\[0\]`, trimmedArg); matched { + + if matched, _ := regexp.MatchString(`\[0\]`, args[0]); matched { internal.Log(1, "XPath indexing of elements starts with 1, not 0") } - if trimmedArg != "" { - path = trimmedArg + if args[0] != "" { + path = args[0] } + parts := strings.Split(path, "/") if parts[0] != "opnsense" { path = "opnsense/" + path } - configElement := configdoc.FindElement(path) - stagingElement := stagingdoc.FindElement(path) - - if stagingElement != nil { - if configElement != nil { - stagingParent := stagingElement.Parent() - stagingParent.RemoveChild(stagingElement) - stagingParent.AddChild(configElement.Copy()) - } else { - stagingParent := stagingElement.Parent() - stagingParent.RemoveChild(stagingElement) + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + stagingEl := stagingdoc.FindElement(path) + configEl := configdoc.FindElement(path) + + if configEl == nil && stagingEl != nil { + // Element is new in staging, remove it + parent := stagingEl.Parent() + parent.RemoveChild(stagingEl) + + // Remove the last part of the path + lastSlash := strings.LastIndex(path, "/") + if lastSlash != -1 { + path = path[:lastSlash] + } + } else if configEl != nil && stagingEl != nil { + // Element exists in both configdoc and stagingdoc, restore it + parent := stagingEl.Parent() + parent.RemoveChild(stagingEl) + parent.AddChild(configEl.Copy()) + + // Restore attributes + configAttrs := configEl.Attr + stagingEl = parent.FindElement(configEl.Tag) + if stagingEl != nil { + for _, attr := range configAttrs { + stagingEl.CreateAttr(attr.Key, attr.Value) + } + } + } else if configEl != nil && stagingEl == nil { + // Element exists in configdoc but not in stagingdoc, add it to stagingdoc + stagingdoc.Root().AddChild(configEl.Copy()) + + // Copy attributes + configAttrs := configEl.Attr + stagingEl = stagingdoc.Root().FindElement(configEl.Tag) + if stagingEl != nil { + for _, attr := range configAttrs { + stagingEl.CreateAttr(attr.Key, attr.Value) + } } - } else { - stagingdoc.Root().AddChild(configElement.Copy()) } } - internal.SaveXMLFile(stagingfile, stagingdoc, host, true) - fmt.Printf("Discarded all staged changes in %s\n", path) + if len(args) < 1 { + fmt.Printf("Discarded all staged configuration changes") + } else { + fmt.Printf("Discarded staged changes in node %s:\n\n", path) + deltadoc := internal.DiffXML(configdoc, stagingdoc, true) + internal.PrintDocument(deltadoc, path) + } + internal.SaveXMLFile(stagingfile, stagingdoc, host, true) }, } diff --git a/cmd/export.go b/cmd/export.go new file mode 100644 index 0000000..92356a8 --- /dev/null +++ b/cmd/export.go @@ -0,0 +1,91 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "fmt" + "strings" + + "github.com/mihakralj/opnsense/internal" + "github.com/spf13/cobra" +) + +// compareCmd represents the compare command +var exportCmd = &cobra.Command{ + Use: "export [] []", + Short: `Export differences between two XML configuration files`, + Long: `The 'export' command generates a patch XML between two configuration files for the OPNsense firewall system. When only one filename is provided, it exports differences between that file and the current 'config.xml'. When no filenames are provided, it exports the patch from current 'config.xml' to 'staging.xml'`, + Example: ` opnsense export b1.xml b2.xml Exports XML patch from 'b1.xml' to 'b2.xml' + opnsense export backup.xml Exports XML patch from 'backup.xml' to 'config.xml' + opnsense export Exports XML patch from 'config.xml' to 'staging.xml'`, + + Run: func(cmd *cobra.Command, args []string) { + internal.SetFlags(verbose, force, host, configfile, nocolor, depth, xmlFlag, yamlFlag, jsonFlag) + var oldconfig, newconfig, path string + + switch len(args) { + case 3: + oldconfig = "/conf/backup/" + args[0] + newconfig = "/conf/backup/" + args[1] + path = strings.Trim(args[2], "/") + case 2: + if strings.HasSuffix(args[1], ".xml") { + oldconfig = "/conf/backup/" + args[0] + newconfig = "/conf/backup/" + args[1] + path = "opnsense" + } else { + oldconfig = "/conf/backup/" + args[0] + newconfig = "/conf/config.xml" + path = strings.Trim(args[1], "/") + } + case 1: + if strings.HasSuffix(args[0], ".xml") { + newconfig = "/conf/config.xml" + oldconfig = "/conf/backup/" + args[0] + path = "opnsense" + } else { + newconfig = "/conf/staging.xml" + oldconfig = "/conf/config.xml" + path = strings.Trim(args[0], "/") + } + default: + oldconfig = "/conf/config.xml" + newconfig = "/conf/staging.xml" + path = "opnsense" + } + parts := strings.Split(path, "/") + if parts[0] != "opnsense" { + path = "opnsense/" + path + } + + internal.Checkos() + olddoc := internal.LoadXMLFile(oldconfig, host, false) + newdoc := internal.LoadXMLFile(newconfig, host, true) + if newdoc == nil { + newdoc = olddoc + } + + deltadoc := internal.DiffXML(olddoc, newdoc, false) + internal.RemoveChgSpace(deltadoc.Root()) + output := internal.ConfigToXML(deltadoc, path) + fmt.Print(output) + }, +} + +func init() { + + rootCmd.AddCommand(exportCmd) +} diff --git a/cmd/import.go b/cmd/import.go new file mode 100644 index 0000000..42dc150 --- /dev/null +++ b/cmd/import.go @@ -0,0 +1,99 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "bufio" + "fmt" + "io" + "os" + + "github.com/beevik/etree" + "github.com/mihakralj/opnsense/internal" + "github.com/spf13/cobra" +) + +var execute bool + +// compareCmd represents the compare command +var importCmd = &cobra.Command{ + Use: "import", + Short: `Import XML patch and stage it for configuraiton change`, + Long: `The 'import' command allows bulk import of configuration changes by injecting an XML patch file that specifies what to add, or delete in the current configuration. Patch file is in the standard XML format generated by the 'export' command, using namespace tags indicating the type of change (e.g., add:, del:). + +The command reads the patch file from the standard input. You can pipe the patch file into this command: + + cat patch.xml | opnsense import + +Or insert patch using I/O redirection: + + opnsense import < patch.xml + +Once the patch is imported, it is added to currently staged changes in 'staging.xml'. You can review these changes using 'opnsense compare -c' and apply them using 'opnsense commit' when ready.`, + + Run: func(cmd *cobra.Command, args []string) { + internal.SetFlags(verbose, force, host, configfile, nocolor, depth, xmlFlag, yamlFlag, jsonFlag) + + patchdoc := etree.NewDocument() + + stat, _ := os.Stdin.Stat() + if (stat.Mode() & os.ModeCharDevice) == 0 { + reader := bufio.NewReader(os.Stdin) + var output []rune + + for { + input, _, err := reader.ReadRune() + if err != nil && err == io.EOF { + break + } + output = append(output, input) + } + err := patchdoc.ReadFromString(string(output)) + if err != nil { + internal.Log(1, "received stdin is not in XML format") + } + + } else { + internal.Log(1, "No data received on stdin, please pipe the XML file into this command") + } + + internal.Checkos() + configdoc := internal.LoadXMLFile(configfile, host, false) + stagingdoc := internal.LoadXMLFile(stagingfile, host, true) + if stagingdoc == nil { + stagingdoc = internal.LoadXMLFile(stagingfile, host, true) + } + + internal.PatchElements(patchdoc.Root(), stagingdoc) + deltadoc := internal.DiffXML(configdoc, stagingdoc, false) + + if !execute { + fmt.Println("Preview of modifications scheduled for imported into staging.xml:") + } + internal.PrintDocument(deltadoc, "opnsense") + + if execute { + internal.SaveXMLFile(stagingfile, stagingdoc, host, true) + fmt.Println("\nModifications imported into staging.xml") + } + }, +} + +func init() { + importCmd.Flags().IntVarP(&depth, "depth", "d", 1, "Specifies number of depth levels of returned tree (default: 1)") + importCmd.Flags().BoolVarP(&execute, "execute", "e", false, "Apply the changes to the staging.xml, rather than just previewing them.") + rootCmd.AddCommand(importCmd) +} diff --git a/cmd/load.go b/cmd/load.go index 7f1ac74..797026b 100644 --- a/cmd/load.go +++ b/cmd/load.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 MihaK mihak09@gmail.com +Copyright © 2023 Miha miha.kralj@outlook.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -60,17 +60,27 @@ var restoreCmd = &cobra.Command{ } filename = "/conf/backup/"+filename internal.Checkos() - configdoc := internal.LoadXMLFile(filename, host) - if configdoc == nil { - internal.Log(1,"failed to get data from %s",filename) + + configdoc := internal.LoadXMLFile(configfile, host, false) + saveddoc := internal.LoadXMLFile(filename, host, false) + + depthset := cmd.LocalFlags().Lookup("depth") + if depthset != nil && !depthset.Changed { + internal.FullDepth() } - internal.Log(2, "Load %s into /conf/staging.xml.",filename) - internal.SaveXMLFile(stagingfile, configdoc, host, true) - fmt.Printf("The file %s has been loaded into /conf/staging.xml.\n", filename) + + deltadoc := internal.DiffXML(configdoc, saveddoc, false) + + internal.PrintDocument(deltadoc, "opnsense") + + internal.Log(2, "Stage %s into %s",filename, stagingfile) + internal.SaveXMLFile(stagingfile, saveddoc, host, true) + fmt.Printf("The file %s has been staged into %s.\n", filename, stagingfile) }, } func init() { + restoreCmd.Flags().IntVarP(&depth, "depth", "d", 1, "Specifies number of depth levels of returned tree (default: 1)") rootCmd.AddCommand(restoreCmd) } diff --git a/cmd/root.go b/cmd/root.go index b78a0fa..1fb9133 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,3 +1,18 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package cmd import ( @@ -9,7 +24,7 @@ import ( ) var ( - Version string = "0.12.1" + Version string = "0.13.0" verbose int force bool host string @@ -32,7 +47,6 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&yamlFlag, "yaml", "y", false, "Output results in YAML format") rootCmd.PersistentFlags().BoolVarP(&force, "force", "f", false, "Bypass checks and prompts (force action)") rootCmd.Flags().BoolVarP(&ver_flag, "version", "V", false, "Display the version of opnsense") - //rootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) cobra.OnInitialize(func() { diff --git a/cmd/run.go b/cmd/run.go index c3c5cf0..ba95947 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,3 +1,18 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package cmd import ( diff --git a/cmd/save.go b/cmd/save.go index bc6ed2e..a227bd7 100644 --- a/cmd/save.go +++ b/cmd/save.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 MihaK mihak09@gmail.com +Copyright © 2023 Miha miha.kralj@outlook.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,10 +17,8 @@ package cmd import ( "fmt" - "math/rand" "regexp" "strings" - "time" "github.com/mihakralj/opnsense/internal" "github.com/spf13/cobra" @@ -30,14 +28,14 @@ import ( var saveCmd = &cobra.Command{ Use: "save [filename]", Short: "Create a new backup XML configuration in '/conf/backup' directory", - Long: `The 'save' command generates a new backup of the existing configuration, storing it in the '/conf/backup' directory. You can specify a filename for the backup, or if no filename is provided, the system will generate a default name based on the current epoch time.`, + Long: `The 'save' command generates a new backup of the existing configuration, storing it in the '/conf/backup' directory. You can specify a filename for the backup, or if no filename is provided, the system will generate a default name based on the current epoch time.`, Example: ` opnsense save Save current config as '/conf/backup/config-.xml' opnsense save filename.xml Save current config as '/conf/backup/filename.xml'`, Run: func(cmd *cobra.Command, args []string) { filename := "" if len(args) < 1 { - filename = generateBackupFilename() + filename = internal.GenerateBackupFilename() } else { filename = args[0] filename = strings.TrimPrefix(filename, "/conf/backup/") @@ -52,12 +50,10 @@ var saveCmd = &cobra.Command{ } } - filename = "/conf/backup/"+filename + filename = "/conf/backup/" + filename internal.Checkos() - configdoc := internal.LoadXMLFile(configfile, host) - if configdoc == nil { - internal.Log(1,"failed to get data from %s",configfile) - } + configdoc := internal.LoadXMLFile(configfile, host, false) + internal.SaveXMLFile(filename, configdoc, host, false) fmt.Printf("%s saved to %s\n", configfile, filename) }, @@ -66,10 +62,3 @@ var saveCmd = &cobra.Command{ func init() { rootCmd.AddCommand(saveCmd) } - -func generateBackupFilename() string { - timestamp := time.Now().Unix() - randomNumber := rand.Intn(10000) - filename := fmt.Sprintf("config-%d.%04d.xml", timestamp, randomNumber) - return filename -} diff --git a/cmd/set.go b/cmd/set.go index b4e99ab..490a94d 100644 --- a/cmd/set.go +++ b/cmd/set.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 MihaK mihak09@gmail.com +Copyright © 2023 Miha miha.kralj@outlook.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,40 +32,32 @@ var deleteFlag bool = false // setCmd represents the set command var setCmd = &cobra.Command{ Use: "set <(att=value)>", - Short: "Set a value or attribute for a node in 'staging.xml'", - Long: `The 'set' command modifies a specific node in the 'staging.xml' file by assigning a new value or attribute. These changes are staged and will not take effect until the 'commit' command is executed to move 'staging.xml' to 'config.xml'. You can discard any changes using the 'discard' command. + Short: "Set a value or attribute for a element in 'staging.xml'", + Long: `The 'set' command modifies a specific element in the 'staging.xml' file by assigning a new value or attribute. These changes are staged and will not take effect until the 'commit' command is executed to move 'staging.xml' to 'config.xml'. You can discard any changes using the 'discard' command. -The XPath parameter offers node targeting, enabling you to navigate to the exact node to modify in the XML structure.`, - Example: ` opnsense set interfaces/wan/if igb0 Set the 'interfaces/wan/if' node to 'igb0' +The XPath parameter offers node targeting, enabling you to navigate to the exact element to modify in the XML structure.`, + Example: ` opnsense set interfaces/wan/if igb0 Set the 'interfaces/wan/if' element to 'igb0' opnsense set system/hostname myrouter Assign 'myrouter' as the hostname in 'staging.xml' - opnsense set interfaces "(version=2.0)" Assign an attribute to the node - opnsense set system/hostname -d Remove the 'system/hostname' node and all its contents`, + opnsense set interfaces "(version=2.0)" Assign an attribute to the element + opnsense set system/hostname -d Remove the 'system/hostname' element and all its contents`, Run: func(cmd *cobra.Command, args []string) { - internal.Checkos() - - configdoc := internal.LoadXMLFile(configfile, host) - if configdoc == nil { - internal.Log(1,"failed to get data from %s",configfile) + if len(args) == 0 { + internal.Log(1, "XPath not provided") + return } + + internal.Checkos() + configdoc := internal.LoadXMLFile(configfile, host, false) internal.EnumerateListElements(configdoc.Root()) - stagingdoc := internal.LoadXMLFile(stagingfile, host) + stagingdoc := internal.LoadXMLFile(stagingfile, host, true) if stagingdoc == nil { stagingdoc = configdoc } internal.EnumerateListElements(stagingdoc.Root()) - if stagingdoc.Root() == nil { - stagingdoc = configdoc - } - - if len(args) == 0 { - internal.Log(1, "XPath not provided") - return - } - path := strings.Trim(args[0], "/") if !strings.HasPrefix(path, "opnsense/") { path = "opnsense/" + path @@ -79,7 +71,6 @@ The XPath parameter offers node targeting, enabling you to navigate to the exact } var attribute, value string - if len(args) == 2 { if isAttribute(args[1]) { attribute = escapeXML(args[1]) @@ -108,7 +99,6 @@ The XPath parameter offers node targeting, enabling you to navigate to the exact } element := stagingdoc.FindElement(path) - if !deleteFlag { if element == nil { element = stagingdoc.Root() @@ -132,10 +122,11 @@ The XPath parameter offers node targeting, enabling you to navigate to the exact } part = fmt.Sprintf("%s.%d", part, maxIndex+1) } - element.CreateElement(part) + + newEl := element.CreateElement(part) + fmt.Printf("Created a new element %s:\n\n", strings.TrimPrefix(newEl.GetPath(), "/")) } element = element.SelectElement(part) - fmt.Println(part, element.GetPath()) } path = element.GetPath() } @@ -146,6 +137,8 @@ The XPath parameter offers node targeting, enabling you to navigate to the exact } element.SetText(value) path = element.GetPath() + fmt.Printf(`Set value "%s" of element %s:`+"\n\n", value, strings.TrimPrefix(path, "/")) + } if attribute != "" { attribute = strings.Trim(attribute, "()") // remove parentheses @@ -154,6 +147,8 @@ The XPath parameter offers node targeting, enabling you to navigate to the exact key := fixXMLName(parts[0]) val := escapeXML(parts[1]) element.CreateAttr(key, val) + fmt.Printf("Set an attribute \"(%s=%s)\" of element %s:\n\n", key, val, path) + } else { internal.Log(1, "Invalid attribute format") } @@ -163,6 +158,7 @@ The XPath parameter offers node targeting, enabling you to navigate to the exact parent := element.Parent() if parent != nil { parent.RemoveChild(element) + fmt.Printf("Deleted element %s:\n\n", path) path = parent.GetPath() } else { internal.Log(1, "Cannot delete the root element") @@ -170,6 +166,7 @@ The XPath parameter offers node targeting, enabling you to navigate to the exact } if value != "" { element.SetText("") + fmt.Printf("Deleted value of element %s:\n\n", path) path = element.GetPath() } if attribute != "" { @@ -178,33 +175,30 @@ The XPath parameter offers node targeting, enabling you to navigate to the exact if len(parts) == 2 { key := fixXMLName(parts[0]) element.RemoveAttr(key) - fmt.Println("deleted attribute", key) + fmt.Printf("Deleted an attribute (%s) of element %s:\n\n", key, path) } else { internal.Log(1, "Invalid attribute format") } } } + deltadoc := internal.DiffXML(configdoc, stagingdoc, true) + //internal.FullDepth() - internal.ReverseEnumerateListElements(configdoc.Root()) - internal.ReverseEnumerateListElements(stagingdoc.Root()) - re := regexp.MustCompile(`\.(\d+)`) - path = re.ReplaceAllString(path, "[$1]") internal.PrintDocument(deltadoc, path) - internal.SaveXMLFile(stagingfile, stagingdoc, host, true) }, } func init() { rootCmd.AddCommand(setCmd) - setCmd.Flags().BoolVarP(&deleteFlag, "delete", "d", false, "Delete a node") + setCmd.Flags().BoolVarP(&deleteFlag, "delete", "d", false, "Delete an element") } func isAttribute(s string) bool { - re := regexp.MustCompile(`^\([^=]+=[^=]+\)$`) + re := regexp.MustCompile(`^\([^=]+(=[^=]*)?\)$`) return re.MatchString(s) } diff --git a/cmd/show.go b/cmd/show.go index f88670f..2bbf95f 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -1,5 +1,5 @@ /* -Copyright © 2023 MihaK mihak09@gmail.com +Copyright © 2023 Miha miha.kralj@outlook.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -51,16 +51,10 @@ var showCmd = &cobra.Command{ internal.Checkos() - configdoc := internal.LoadXMLFile(configfile, host) - if configdoc.Root() == nil { - internal.Log(1, "failed to get data from %s", configfile) - } - stagingdoc := internal.LoadXMLFile(stagingfile, host) - + configdoc := internal.LoadXMLFile(configfile, host, false) + stagingdoc := internal.LoadXMLFile(stagingfile, host, true) if stagingdoc == nil { stagingdoc = configdoc - internal.Log(4, "failed to get data from %s, using %s", stagingfile, configfile) - } deltadoc := internal.DiffXML(configdoc, stagingdoc, true) diff --git a/cmd/sysinfo.go b/cmd/sysinfo.go index 3476e7b..3e205b3 100644 --- a/cmd/sysinfo.go +++ b/cmd/sysinfo.go @@ -1,5 +1,17 @@ /* -Copyright © 2023 NAME HERE +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package cmd diff --git a/internal/parser.go b/internal/ConfigToOutput.go similarity index 64% rename from internal/parser.go rename to internal/ConfigToOutput.go index c46d8dd..8b13eca 100644 --- a/internal/parser.go +++ b/internal/ConfigToOutput.go @@ -1,3 +1,18 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package internal import ( @@ -5,34 +20,12 @@ import ( "strings" "github.com/beevik/etree" - "github.com/clbanning/mxj" "gopkg.in/yaml.v3" ) -func EtreeToJSON(el *etree.Element) (string, error) { - doc := etree.NewDocument() - doc.SetRoot(el.Copy()) - - str, err := doc.WriteToString() - if err != nil { - return "", err - } - mv, err := mxj.NewMapXml([]byte(str)) // parse xml to map - if err != nil { - return "", err - } - - jsonStr, err := mv.JsonIndent("", " ") // convert map to json - if err != nil { - return "", err - } - - return string(jsonStr), nil -} - -////////// - func ConfigToTTY(doc *etree.Document, path string) string { + + path = strings.TrimPrefix(path, "/") focused := FocusEtree(doc, path) d := depth + len(strings.Split(path, "/")) - 1 if len(doc.FindElements(path)) > 1 { diff --git a/internal/xmlcompare.go b/internal/DiffXML.go similarity index 63% rename from internal/xmlcompare.go rename to internal/DiffXML.go index b095490..ee65d5c 100644 --- a/internal/xmlcompare.go +++ b/internal/DiffXML.go @@ -1,3 +1,18 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package internal import ( @@ -8,17 +23,108 @@ import ( ) // DiffXML compares two etree documents and returns a new document with only the changed elements. -func DiffXML(oldDoc, newDoc *etree.Document, compare bool) *etree.Document { +func DiffXML(oldDoc, newDoc *etree.Document, fulltree bool) *etree.Document { + diffDoc := oldDoc.Copy() + EnumerateListElements(newDoc.Root()) - EnumerateListElements(oldDoc.Root()) + EnumerateListElements(diffDoc.Root()) - addMissingElements(newDoc.Root(), oldDoc) - checkElements(oldDoc.Root(), newDoc) + addMissingElements(newDoc.Root(), diffDoc) + checkElements(diffDoc.Root(), newDoc) - ReverseEnumerateListElements(oldDoc.Root()) + ReverseEnumerateListElements(diffDoc.Root()) ReverseEnumerateListElements(newDoc.Root()) - return oldDoc + if !fulltree { + removeNodesWithoutSpace(diffDoc.Root()) + removeAttSpace(diffDoc.Root()) + } + return diffDoc +} + +// removeNodesWithoutSpace recursively removes elements without a "Space" attribute +func removeNodesWithoutSpace(el *etree.Element) { + for i := 0; i < len(el.Child); i++ { + child, ok := el.Child[i].(*etree.Element) + if !ok { + continue + } + + // Check if any attribute has Space defined + hasAttrWithSpace := false + for _, attr := range child.Attr { + if attr.Space != "" { + hasAttrWithSpace = true + break + } + } + + // Remove the child element only if it doesn't have a Space and none of its attributes have a Space + if child.Space == "" && !hasAttrWithSpace { + el.RemoveChildAt(i) + i-- // Adjust index because we've removed an item + continue + } + + removeNodesWithoutSpace(child) + } +} + +func removeAttSpace(el *etree.Element) { + if el == nil { + return + } + + // Remove or unset the "Space" attribute if it is set to "att" + if el.Space == "att" { + el.Space = "" + } + + // Recursively process children + for i := 0; i < len(el.Child); i++ { + child, ok := el.Child[i].(*etree.Element) + if !ok { + continue // Skip if this child is not an Element + } + + removeAttSpace(child) + } +} + +func RemoveChgSpace(el *etree.Element) { + if el == nil { + return + } + + // Remove or unset the "Space" attribute if it is set to "att" + if el.Space == "chg" { + parts := strings.Split(el.Text(), "|||") + if len(parts) > 1 { + el.SetText(parts[1]) + } + el.Space = "add" + } + // Process attributes + for i := range el.Attr { + // Check if the attribute space is "chg" + if el.Attr[i].Space == "chg" { + parts := strings.Split(el.Attr[i].Value, "|||") + if len(parts) > 1 { + el.Attr[i].Value = parts[1] + } + el.Attr[i].Space = "add" + } + } + + // Recursively process children + for i := 0; i < len(el.Child); i++ { + child, ok := el.Child[i].(*etree.Element) + if !ok { + continue // Skip if this child is not an Element + } + + RemoveChgSpace(child) + } } func checkElements(oldEl *etree.Element, newDoc *etree.Document) { @@ -33,8 +139,10 @@ func checkElements(oldEl *etree.Element, newDoc *etree.Document) { oldEl.Space = "chg" oldEl.SetText(fmt.Sprintf("%s|||%s", oldElText, newElText)) markParentSpace(oldEl) - } else if newElText != "" { - oldEl.SetText(newEl.Text()) + } else if newElText != "" && oldElText == ""{ + oldEl.Space = "chg" + oldEl.SetText("N/A|||"+newEl.Text()) + markParentSpace(oldEl) } } copyAttributes(newEl, oldEl) @@ -78,7 +186,7 @@ func addMissingElements(newEl *etree.Element, oldDoc *etree.Document) { parentInOldDoc := oldDoc.FindElement(parentPath) if parentInOldDoc != nil { - oldEl := etree.NewElement(fmt.Sprintf("new:%s", newEl.Tag)) + oldEl := etree.NewElement(fmt.Sprintf("add:%s", newEl.Tag)) oldEl.SetText(newEl.Text()) copyAttributes(newEl, oldEl) @@ -115,7 +223,7 @@ func copyAttributes(oldEl, newEl *etree.Element) { // If same value, do nothing } else { // Attribute does not exist in newEl, add with namespace del: - newEl.CreateAttr(fmt.Sprintf("new:%s", oldAttr.Key), oldAttr.Value) + newEl.CreateAttr(fmt.Sprintf("add:%s", oldAttr.Key), oldAttr.Value) markParentSpace(newEl) } } diff --git a/internal/EtreeToJSON.go b/internal/EtreeToJSON.go new file mode 100644 index 0000000..336a483 --- /dev/null +++ b/internal/EtreeToJSON.go @@ -0,0 +1,42 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package internal + +import ( + "github.com/beevik/etree" + "github.com/clbanning/mxj" +) + +func EtreeToJSON(el *etree.Element) (string, error) { + doc := etree.NewDocument() + doc.SetRoot(el.Copy()) + + str, err := doc.WriteToString() + if err != nil { + return "", err + } + mv, err := mxj.NewMapXml([]byte(str)) // parse xml to map + if err != nil { + return "", err + } + + jsonStr, err := mv.JsonIndent("", " ") // convert map to json + if err != nil { + return "", err + } + + return string(jsonStr), nil +} diff --git a/internal/EtreeToTTY.go b/internal/EtreeToTTY.go new file mode 100644 index 0000000..89a1103 --- /dev/null +++ b/internal/EtreeToTTY.go @@ -0,0 +1,124 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package internal + +import ( + "fmt" + "regexp" + "strings" + + "github.com/beevik/etree" +) + +// EtreeToTTY returns a string representation of the etree.Element in a TTY-friendly format. +func EtreeToTTY(el *etree.Element, level int, indent int) string { + // Enumerate list elements + EnumerateListElements(el) + + // Set the indentation string for hierarchy + indentation := strings.Repeat(" ", indent) + + // Set the line prefix based on the element's space + var result strings.Builder + + // lead stores the leading spaces before root tag + lead := c["nil"] + " " + linePrefix := "" + switch el.Space { + case "att": + linePrefix = c["chg"] + "!" + lead + c["chg"] + case "add": + linePrefix = c["add"] + "+" + lead + indentation = indentation + c["grn"] + case "chg": + linePrefix = c["chg"] + "~" + lead + c["chg"] + case "del": + linePrefix = c["del"] + "-" + lead + indentation = indentation + c["del"] + default: + linePrefix = c["tag"] + " " + lead + c["tag"] + } + + // Build the attribute string + var attributestr, chgstr string + for _, attr := range el.Attr { + switch { + case attr.Space == "del": + attributestr += " " + c["ita"] + c["del"] + fmt.Sprintf("(%s=\"%s\")"+c["nil"], attr.Key, attr.Value) + if el.Space == "" { + linePrefix = c["del"] + "-" + lead + c["nil"] + } + case attr.Space == "add": + attributestr += " " + c["ita"] + c["add"] + fmt.Sprintf("(%s=\"%s\")"+c["nil"], attr.Key, attr.Value) + if el.Space == "" { + linePrefix = c["add"] + "+" + lead + c["nil"] + } + case attr.Space == "chg": + attributestr += c["tag"] + " (" + c["ita"] + c["chg"] + fmt.Sprintf("%s"+c["tag"]+"=\""+c["del"]+"%s"+c["tag"]+"\")"+c["nil"], attr.Key, strings.Replace(attr.Value, "|||", c["nil"]+c["tag"]+"\""+c["arw"]+"\""+c["grn"], 1)) + if el.Space == "" { + linePrefix = c["chg"] + "~" + lead + c["nil"] + } + default: + attributestr += c["tag"] + " (" + c["ita"] + c["atr"] + fmt.Sprintf("%s"+c["tag"]+"=\""+c["atr"]+"%s"+c["tag"]+"\")"+c["nil"], attr.Key, attr.Value) + } + } + + // Replace ".n" with "[n]" in the tag name + match, _ := regexp.MatchString(`\.\d+$`, el.Tag) + if match { + lastIndex := strings.LastIndex(el.Tag, ".") + el.Tag = el.Tag[:lastIndex] + "[" + el.Tag[lastIndex+1:] + "]" + } + + // Build the content string + if len(el.ChildElements()) > 0 { + // If the element has child elements, build a block of nested elements + result.WriteString(linePrefix + indentation + el.Tag + ":" + c["atr"] + attributestr + c["tag"] + " {" + c["nil"]) + + if level > 0 { + result.WriteString("\n") + for _, child := range el.ChildElements() { + result.WriteString(EtreeToTTY(child, level-1, indent+1)) + } + result.WriteString(lead + " " + indentation + c["tag"] + "}" + c["nil"] + "\n") + } else { + result.WriteString(c["nil"] + c["txt"] + c["ell"] + c["tag"] + "}\n") + } + + } else { + // If the element has no child elements, build a single-line representation + elText := el.Text() + switch el.Space { + case "chg": + elText = c["nil"] + c["del"] + strings.Replace(elText, "|||", c["nil"]+c["arw"]+c["grn"], 1) + case "del": + elText = c["nil"] + c["del"] + strings.TrimSpace(elText) + case "add": + elText = c["nil"] + c["grn"] + strings.TrimSpace(elText) + default: + elText = c["nil"] + c["txt"] + strings.TrimSpace(elText) + } + content := chgstr + elText + c["nil"] + if el.Parent().GetPath() == "/" && len(el.ChildElements()) == 0 { + result.WriteString(linePrefix + indentation + el.Tag + ": {\n" + linePrefix + "}") + + } else { + result.WriteString(linePrefix + indentation + el.Tag + ":" + c["atr"] + attributestr + c["nil"] + " " + content + c["nil"] + "\n") + } + } + + return result.String() +} diff --git a/internal/FocusEtree.go b/internal/FocusEtree.go new file mode 100644 index 0000000..b89a6fd --- /dev/null +++ b/internal/FocusEtree.go @@ -0,0 +1,77 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package internal + +import ( + "strings" + + "github.com/beevik/etree" +) + +// FocusEtree returns an etree.Element that represents the specified path in the document. +// It removes all other branches above the element that are not on the path +// If the path does not exist, it returns nil. +func FocusEtree(doc *etree.Document, path string) *etree.Element { + path = strings.TrimPrefix(path, "/") + + // Find all elements that match the path + foundElements := doc.FindElements(path) + if len(foundElements) == 0 { + Log(1, "Xpath element \"%s\" does not exist", path) + return nil + } + + // Create a new element to represent the focused path + parts := strings.Split(path, "/") + focused := etree.NewElement(parts[0]) + + // Get the space of the found element + space := foundElements[0].Space + depth := len(parts) + if depth > 1 { + // Create child elements for each part of the path + parts = parts[:depth-1] + current := focused + for i := 1; i < len(parts); i++ { + newElem := current.CreateElement(parts[i]) + // Find the element in the document and copy its attributes + element := doc.FindElement(strings.Join(parts[:i+1], "/")) + space = element.Space + if space != "" { + newElem.Space = space + } + if element != nil { + for _, attr := range element.Attr { + newElem.CreateAttr(attr.Key, attr.Value) + } + } + current = newElem + } + // Add all found elements as children of the last child element + for _, foundElement := range foundElements { + current.AddChild(foundElement.Copy()) + } + } else { + // If the path is just the root element, return the root element of the document + focused = doc.Root() + } + if space != "" { + // Set the space of the focused element to "att" + focused.Space = "att" + Log(5, "element maked with attention flag: %s", focused.GetPath()) + } + return focused +} diff --git a/internal/LoadXMLFile.go b/internal/LoadXMLFile.go new file mode 100644 index 0000000..c0b7d50 --- /dev/null +++ b/internal/LoadXMLFile.go @@ -0,0 +1,45 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package internal + +import ( + "fmt" + "strings" + + "github.com/beevik/etree" +) + +func LoadXMLFile(filename string, host string, canBeMissing bool) *etree.Document { + if !strings.HasSuffix(filename, ".xml") { + Log(1, "filename %s does not end with .xml", filename) + } + doc := etree.NewDocument() + bash := fmt.Sprintf(`test -f "%s" && cat "%s" || echo "missing"`, filename, filename) + content := ExecuteCmd(bash, host) + if strings.TrimSpace(content) == "missing" { + if canBeMissing { + return nil + } else { + Log(1, "failed to get data from %s", filename) + } + } + err := doc.ReadFromString(content) + if err != nil { + Log(1, "%s is not an XML file", filename) + } + return doc + +} diff --git a/internal/PatchXML.go b/internal/PatchXML.go new file mode 100644 index 0000000..49de130 --- /dev/null +++ b/internal/PatchXML.go @@ -0,0 +1,85 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package internal + +import ( + "strings" + + "github.com/beevik/etree" +) + +func PatchElements(patchEl *etree.Element, newDoc *etree.Document) { + if patchEl == nil { + return + } + + // Process elements + switch patchEl.Space { + case "add", "del": + InjectElementAtPath(patchEl, newDoc) + } + + // Process attributes + for _, attr := range patchEl.Attr { + switch attr.Space { + case "add", "del": + InjectElementAtPath(patchEl, newDoc) + } + } + // Recursively process child elements + for _, child := range patchEl.ChildElements() { + PatchElements(child, newDoc) + } +} + +func InjectElementAtPath(el *etree.Element, doc *etree.Document) { + + match := doc.FindElement(el.GetPath()) + + if match == nil { + current := doc.Root() + parts := strings.Split(el.GetPath(), "/") + // No match found in doc, we need to create the new path + for i := 2; i < len(parts); i++ { + match = current.SelectElement(parts[i]) + if match == nil { + match = current.CreateElement(parts[i]) + } + current = match + } + if el.Space == "add" { + match.SetText(el.Text()) + } + } else { + if el.Space == "add" { + match.SetText(el.Text()) + } + if el.Space == "del" { + match.Parent().RemoveChild(match) + } + } + + for _, attr := range el.Attr { + if attr.Space == "add" { + match.CreateAttr(attr.Key, attr.Value) + } + if attr.Space == "del" { + match.RemoveAttr(attr.Key) + } + + } + +} diff --git a/internal/PrintDocument.go b/internal/PrintDocument.go new file mode 100644 index 0000000..5282fae --- /dev/null +++ b/internal/PrintDocument.go @@ -0,0 +1,37 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package internal + +import ( + "fmt" + + "github.com/beevik/etree" +) + +func PrintDocument(doc *etree.Document, path string) { + var output string + switch { + case xmlFlag: + output = ConfigToXML(doc, path) + case jsonFlag: + output = ConfigToJSON(doc, path) + case yamlFlag: + output = ConfigToYAML(doc, path) + default: + output = ConfigToTTY(doc, path) + } + fmt.Println(output) +} diff --git a/internal/printloadsave.go b/internal/SaveXMLFile.go similarity index 52% rename from internal/printloadsave.go rename to internal/SaveXMLFile.go index 34d9b50..f54661e 100644 --- a/internal/printloadsave.go +++ b/internal/SaveXMLFile.go @@ -1,44 +1,29 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package internal import ( "fmt" + "math/rand" "strings" + "time" "github.com/beevik/etree" ) -func PrintDocument(doc *etree.Document, path string) { - var output string - switch { - case xmlFlag: - output = ConfigToXML(doc, path) - case jsonFlag: - output = ConfigToJSON(doc, path) - case yamlFlag: - output = ConfigToYAML(doc, path) - default: - output = ConfigToTTY(doc, path) - } - fmt.Println(output) -} - -func LoadXMLFile(filename string, host string) *etree.Document { - if !strings.HasSuffix(filename, ".xml") { - Log(1, "filename %s does not end with .xml", filename) - } - doc := etree.NewDocument() - bash := fmt.Sprintf(`test -f "%s" && cat "%s" || echo "missing"`, filename, filename) - content := ExecuteCmd(bash, host) - if strings.TrimSpace(content) != "missing" { - err := doc.ReadFromString(content) - if err != nil { - Log(1, "%s is not an XML file", filename) - } - return doc - } - return nil -} - func SaveXMLFile(filename string, doc *etree.Document, host string, forced bool) { configout := ConfigToXML(doc, "opnsense") bash := `test -f "` + filename + `" && echo "exists" || echo "missing"` @@ -52,7 +37,6 @@ func SaveXMLFile(filename string, doc *etree.Document, host string, forced bool) bash = "sudo rm " + filename ExecuteCmd(bash, host) } - sftpCmd(configout, filename, host) // check that file was written @@ -65,3 +49,10 @@ func SaveXMLFile(filename string, doc *etree.Document, host string, forced bool) Log(1, "error writing file %s", filename) } } + +func GenerateBackupFilename() string { + timestamp := time.Now().Unix() + randomNumber := rand.Intn(10000) + filename := fmt.Sprintf("config-%d.%04d.xml", timestamp, randomNumber) + return filename +} diff --git a/internal/checkos.go b/internal/checkos.go index a3cc13a..d8e9631 100644 --- a/internal/checkos.go +++ b/internal/checkos.go @@ -1,9 +1,25 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package internal import ( "strings" ) +// Checkos checks that the target is an OPNsense system func Checkos() (string, error) { //check that the target is OPNsense osstr := ExecuteCmd("echo `uname` `opnsense-version -N`", host) @@ -11,6 +27,6 @@ func Checkos() (string, error) { if osstr != "FreeBSD OPNsense" { Log(1, "%s is not OPNsense system", osstr) } - Log(4, "OPNsense detected") + Log(4, "OPNsense system detected") return osstr, nil } diff --git a/internal/color_map.go b/internal/color_map.go new file mode 100644 index 0000000..fb8e092 --- /dev/null +++ b/internal/color_map.go @@ -0,0 +1,46 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package internal + +var c = map[string]string{ + "tag": "\033[0m", + "txt": "\033[33m", + "atr": "\033[33m", + + "chg": "\033[36m", + "add": "\033[32m", + "del": "\033[31m\033[9m", + "red": "\033[31m", + "grn": "\033[32m", + "ele": "\033[36m", + + "yel": "\033[33m", + "blu": "\033[34m", + "mgn": "\033[35m", + "cyn": "\033[36m", + "wht": "\033[37m", + "gry": "\033[90m", + + "ita": "\033[3m", // italics + "bld": "\033[1m", // bold + "stk": "\033[9m", // strikethroough + "und": "\033[4m", + "rev": "\033[7m", // reverse colors + + "ell": "\u2026", + "arw": " \u2192 ", + "nil": "\033[0m", +} diff --git a/internal/eTreeRender.go b/internal/eTreeRender.go deleted file mode 100644 index 7818e17..0000000 --- a/internal/eTreeRender.go +++ /dev/null @@ -1,127 +0,0 @@ -package internal - -import ( - "fmt" - "regexp" - "strings" - - "github.com/beevik/etree" -) - -func FocusEtree(doc *etree.Document, path string) *etree.Element { - foundElements := doc.FindElements(path) - if len(foundElements) == 0 { - Log(1, "Xpath element \"%s\" does not exist", path) - return nil - } - - parts := strings.Split(path, "/") - focused := etree.NewElement(parts[0]) - - // Get the space of the found element - space := foundElements[0].Space - depth := len(parts) - if depth > 1 { - parts = parts[:depth-1] - current := focused - for i := 1; i < len(parts); i++ { - newElem := current.CreateElement(parts[i]) - // Find the element in the document and copy its attributes - element := doc.FindElement(strings.Join(parts[:i+1], "/")) - space = element.Space - if space != "" { - newElem.Space = space - } - if element != nil { - for _, attr := range element.Attr { - newElem.CreateAttr(attr.Key, attr.Value) - } - } - current = newElem - } - // Add all found elements - for _, foundElement := range foundElements { - current.AddChild(foundElement.Copy()) - } - } else { - focused = doc.Root() - } - if space != "" { - focused.Space = "att" - } - - return focused -} - -////////// - -func EtreeToTTY(el *etree.Element, level int, indent int) string { - EnumerateListElements(el) - indentation := strings.Repeat(" ", indent) - var result strings.Builder - linePrefix := "" - - switch el.Space { - case "att": - linePrefix = c["chg"] + "!" + c["nil"] + " " + c["chg"] - case "new": - linePrefix = c["grn"] + "+" + c["nil"] + " " - indentation = indentation + c["grn"] - case "chg": - linePrefix = c["chg"] + "~" + c["nil"] + " " + c["chg"] - case "del": - linePrefix = c["del"] + "-" + c["nil"] + " " - indentation = indentation + c["del"] - default: - linePrefix = c["tag"] + " " + c["nil"] + " " + c["tag"] - } - - var attributestr, chgstr string - for _, attr := range el.Attr { - switch { - case attr.Space == "new": - attributestr += " " + c["ita"] + c["new"] + fmt.Sprintf("(%s=\"%s\")", attr.Key, attr.Value) - case attr.Space == "chg": - attributestr += c["tag"] + " (" + c["ita"] + c["atr"] + fmt.Sprintf("%s=\""+c["del"]+"%s"+c["atr"]+"\")", attr.Key, strings.Replace(attr.Value, "|||", c["atr"]+"\""+c["arw"]+"\""+c["grn"], 1)) - case attr.Space == "del": - attributestr += " " + c["ita"] + c["del"] + fmt.Sprintf("(%s=\"%s\")"+c["nil"], attr.Key, attr.Value) - default: - attributestr += c["tag"] + " (" + c["ita"] + c["atr"] + fmt.Sprintf("%s=\"%s\""+c["tag"]+")", attr.Key, attr.Value) - } - } - match, _ := regexp.MatchString(`\.\d+$`, el.Tag) - if match { - lastIndex := strings.LastIndex(el.Tag, ".") - el.Tag = el.Tag[:lastIndex] + "[" + el.Tag[lastIndex+1:] + "]" - } - if len(el.ChildElements()) > 0 { - result.WriteString(linePrefix + indentation + el.Tag + ":" + c["atr"] + attributestr + c["tag"] + " {" + c["nil"]) - - if level > 0 { - result.WriteString("\n") - for _, child := range el.ChildElements() { - result.WriteString(EtreeToTTY(child, level-1, indent+1)) - } - - result.WriteString(" " + indentation + c["tag"] + "}" + c["nil"] + "\n") - } else { - result.WriteString(c["nil"] + c["txt"] + c["ell"] + c["tag"] + "}\n") - } - - } else { - elText := el.Text() - switch el.Space { - case "chg": - elText = c["nil"] + c["del"] + strings.Replace(elText, "|||", c["nil"]+c["arw"]+c["grn"], 1) - case "del": - elText = c["nil"] + c["del"] + strings.TrimSpace(elText) - case "new": - elText = c["nil"] + c["grn"] + strings.TrimSpace(elText) - default: - elText = c["nil"] + c["txt"] + strings.TrimSpace(elText) - } - content := chgstr + elText + c["nil"] - result.WriteString(linePrefix + indentation + el.Tag + ":" + c["atr"] + attributestr + " " + content + c["nil"] + "\n") - } - return result.String() -} diff --git a/internal/executecmd.go b/internal/executecmd.go index bfe3a34..2d1f5ed 100644 --- a/internal/executecmd.go +++ b/internal/executecmd.go @@ -1,12 +1,24 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package internal import ( "bytes" - "os" "os/exec" "strings" - - "github.com/pkg/sftp" ) func ExecuteCmd(command, host string) string { @@ -14,7 +26,7 @@ func ExecuteCmd(command, host string) string { Log(5, "local shell: %s", command) out, err := exec.Command("sh", "-c", command).Output() if err != nil { - Log(1, "failed to execute command: %s %s", command, err.Error()) + Log(1, "failed to execute command: %s %s", command, err.Error()) } Log(5, "received from local shell: %s", out) @@ -46,49 +58,3 @@ func ExecuteCmd(command, host string) string { Log(5, "received from ssh: %s", strings.TrimRight(stdoutBuf.String(), "\n")) return strings.TrimRight(stdoutBuf.String(), "\n") } - -func sftpCmd(data, filename, host string) { - var sftpClient *sftp.Client - var err error - - if host == "" { - // If host is empty, save the data to a local file - err = os.WriteFile(filename, []byte(data), 0644) - if err != nil { - Log(1, "Failed to save data to local file. %s", err.Error()) - } - Log(4, "Successfully saved data to local file %s", filename) - return - } - - sshClient, err := getSSHClient(host) - if err != nil { - Log(1, "failed to initiate ssh client. %s", err.Error()) - } - - if sshClient.Client == nil { - Log(1, "SSH client is nil. Cannot perform SFTP operation.") - } - - // Create an SFTP client - sftpClient, err = sftp.NewClient(sshClient.Client) - if err != nil { - Log(1, "Failed to initiate SFTP client. %s", err.Error()) - } - defer sftpClient.Close() - - // Create remote file - remoteFile, err := sftpClient.Create(filename) - if err != nil { - Log(1, "Failed to create remote file. %s", err.Error()) - } - defer remoteFile.Close() - - // Write data to remote file - _, err = remoteFile.Write([]byte(data)) - if err != nil { - Log(1, "Failed to write to remote file. %s", err.Error()) - } - - Log(4, "Successfully transferred data to %s on host %s", filename, host) -} diff --git a/internal/ssh.go b/internal/getSSHClient.go similarity index 77% rename from internal/ssh.go rename to internal/getSSHClient.go index d1c4d67..0f9741c 100644 --- a/internal/ssh.go +++ b/internal/getSSHClient.go @@ -1,3 +1,18 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package internal import ( diff --git a/internal/log.go b/internal/log.go index f01dd7e..1014c0b 100644 --- a/internal/log.go +++ b/internal/log.go @@ -1,3 +1,18 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package internal import ( @@ -7,35 +22,6 @@ import ( "strings" ) -var c = map[string]string{ - "tag": "\033[0m", - "txt": "\033[33m", - "atr": "\033[33m", - - "chg": "\033[34m", - "new": "\033[32m", - "del": "\033[31m\033[9m", - "red": "\033[31m", - "grn": "\033[32m", - - "yel": "\033[33m", - "blu": "\033[34m", - "mgn": "\033[35m", - "cyn": "\033[36m", - "wht": "\033[37m", - "gry": "\033[90m", - - "ita": "\033[3m", // italics - "bld": "\033[1m", // bold - "stk": "\033[9m", // strikethroough - "und": "\033[4m", - "rev": "\033[7m", // reverse colors - - "ell": "\u2026", - "arw": " \u2192 ", - "nil": "\033[0m", -} - func Log(verbosity int, format string, args ...interface{}) { levels := []string{"", c["red"] + "Error:\t " + c["nil"], @@ -45,8 +31,8 @@ func Log(verbosity int, format string, args ...interface{}) { c["wht"] + "Debug:\t " + c["nil"]} formatted := fmt.Sprintf(format, args...) - if len(formatted) > 500 { - formatted = formatted[:200] + "\n...\n" + formatted[len(formatted)-200:] + if len(formatted) > 2000 { + formatted = formatted[:1000] + "\n...\n" + formatted[len(formatted)-200:] } message := levels[verbosity] + formatted diff --git a/internal/setflags.go b/internal/setflags.go index e26393b..bafb990 100644 --- a/internal/setflags.go +++ b/internal/setflags.go @@ -1,3 +1,18 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ package internal var ( @@ -34,3 +49,7 @@ func SetFlags(v int, f bool, h string, config string, nc bool, dpt int, x bool, c["arw"] = " -> " } } + +func FullDepth() { + depth = depth+50 +} diff --git a/internal/sftpCmd.go b/internal/sftpCmd.go new file mode 100644 index 0000000..f8689c9 --- /dev/null +++ b/internal/sftpCmd.go @@ -0,0 +1,68 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package internal + +import ( + "os" + + "github.com/pkg/sftp" +) + +func sftpCmd(data, filename, host string) { + var sftpClient *sftp.Client + var err error + + if host == "" { + // If host is empty, save the data to a local file + err = os.WriteFile(filename, []byte(data), 0644) + if err != nil { + Log(1, "Failed to save data to local file. %s", err.Error()) + } + Log(4, "Successfully saved data to local file %s", filename) + return + } + + sshClient, err := getSSHClient(host) + if err != nil { + Log(1, "failed to initiate ssh client. %s", err.Error()) + } + + if sshClient.Client == nil { + Log(1, "SSH client is nil. Cannot perform SFTP operation.") + } + + // Create an SFTP client + sftpClient, err = sftp.NewClient(sshClient.Client) + if err != nil { + Log(1, "Failed to initiate SFTP client. %s", err.Error()) + } + defer sftpClient.Close() + + // Create remote file + remoteFile, err := sftpClient.Create(filename) + if err != nil { + Log(1, "Failed to create remote file. %s", err.Error()) + } + defer remoteFile.Close() + + // Write data to remote file + _, err = remoteFile.Write([]byte(data)) + if err != nil { + Log(1, "Failed to write to remote file. %s", err.Error()) + } + + Log(4, "Successfully transferred data to %s on host %s", filename, host) +} diff --git a/internal/sshAgent_unix.go b/internal/sshAgent_unix.go index ea2bbc5..0674f76 100644 --- a/internal/sshAgent_unix.go +++ b/internal/sshAgent_unix.go @@ -1,3 +1,18 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ //go:build !windows // +build !windows diff --git a/internal/sshAgent_windows.go b/internal/sshAgent_windows.go index 84a6525..e28c828 100644 --- a/internal/sshAgent_windows.go +++ b/internal/sshAgent_windows.go @@ -1,3 +1,18 @@ +/* +Copyright © 2023 Miha miha.kralj@outlook.com + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ //go:build windows // +build windows diff --git a/tt.xml b/tt.xml new file mode 100644 index 0000000..293077c --- /dev/null +++ b/tt.xml @@ -0,0 +1,13 @@ + + + dracula + + + wifi + + + + + + value +