diff --git a/README.md b/README.md index 0e0ab7c..8f49882 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,35 @@ ![txeh - /etc/hosts mangement](txeh.png) -# Etc Hosts Management Library +# Etc Hosts Management Utility & Go Library [![Go Report Card](https://goreportcard.com/badge/github.com/txn2/txeh)](https://goreportcard.com/report/github.com/txn2/txeh) [![GoDoc](https://godoc.org/github.com/txn2/irsync/txeh?status.svg)](https://godoc.org/github.com/txn2/txeh) +## txeh Utility +The txeh CLI application allows command line or scripted access to /etc/hosts file modification. + +Examples: +```bash +# point the hostnames "test" and "test.two" to local loopback +sudo txeh add 127.0.0.1 test test.two + +# remove the hostname "test" +sudo txeh remove test +``` + + + + +### Motivation + +TXEH was build to support [kubefwd](https://github.com/txn2/kubefwd), a Kubernetes port-forwarding utility utilizing [/etc/hosts] to associate custom hostnames with local IP addresses. A computer's [/etc/hosts] file is a powerful utility for developers and system administrators to create localized, custom DNS entries. This small go library was developed to encapsulate the complexity of -working with /etc/hosts by providing a simple interface to load, create, remove and save entries to an /etc/host file. No validation is done on the input data. Validation is considered out of scope for this project, use with caution. +working with /etc/hosts by providing a simple interface to load, create, remove and save entries to an /etc/host file. No validation is done on the input data. **Validation is considered out of scope for this project, use with caution**. + + Basic implemention: ```go @@ -55,3 +75,5 @@ func main() { } ``` + +[/etc/hosts]:https://en.wikipedia.org/wiki/Hosts_(file) \ No newline at end of file diff --git a/txeh.png b/txeh.png index df747f9..f4b485f 100644 Binary files a/txeh.png and b/txeh.png differ diff --git a/util/cmd/add.go b/util/cmd/add.go new file mode 100644 index 0000000..3e7a958 --- /dev/null +++ b/util/cmd/add.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(addCmd) +} + +var addCmd = &cobra.Command{ + Use: "add [IP] [HOSTNAME] [HOSTNAME] [HOSTNAME]...", + Short: "Add hostnames to /etc/hosts", + Long: `Add/Associate one or more hostnames to an IP address `, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return errors.New("the \"add\" command requires an IP address and at least one hostname") + } + + if validateIPAddress(args[0]) == false { + return errors.New("the IP address provided is not a valid ipv4 or ipv6 address") + } + + if ok, hn := validateHostnames(args[1:]); !ok { + return errors.New(fmt.Sprintf("\"%s\" is not a valid hostname", hn)) + } + + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + if !Quiet { + fmt.Printf("Adding host(s) \"%s\" to IP address %s\n", strings.Join(args[1:], " "), args[0]) + } + + AddHosts(args[0], args[1:]) + }, +} + +func AddHosts(ip string, hosts []string) { + + etcHosts.AddHosts(ip, hosts) + + if DryRun { + fmt.Print(etcHosts.RenderHostsFile()) + return + } + + err := etcHosts.Save() + if err != nil { + fmt.Printf("Error: could not save %s. Reason: %s\n", etcHosts.WriteFilePath, err.Error()) + os.Exit(1) + } +} diff --git a/util/cmd/remove.go b/util/cmd/remove.go new file mode 100644 index 0000000..64c609e --- /dev/null +++ b/util/cmd/remove.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(removeCmd) +} + +var removeCmd = &cobra.Command{ + Use: "remove [TYPE] [HOSTNAME|IP] [HOSTNAME|IP] [HOSTNAME|IP]...", + Short: "Remove a hostname or ip address", + Long: `Remove one or more hostnames or ip addresses from /etc/hosts`, + Run: func(cmd *cobra.Command, args []string) { + err := cmd.Help() + if err != nil { + fmt.Printf("Error: can not display help, reason: %s\n", err.Error()) + os.Exit(1) + } + + fmt.Println("Please specify a sub-command such as \"host\" or \"ip\"") + os.Exit(1) + }, +} diff --git a/util/cmd/remove_host.go b/util/cmd/remove_host.go new file mode 100644 index 0000000..37ab567 --- /dev/null +++ b/util/cmd/remove_host.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" +) + +func init() { + removeCmd.AddCommand(removeHostCmd) +} + +var removeHostCmd = &cobra.Command{ + Use: "host [HOSTNAME] [HOSTNAME] [HOSTNAME]...", + Short: "Remove hostnames from /etc/hosts", + Long: `Remove one or more hostnames from /etc/hosts`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("the \"remove hosts\" command requires at least one hostname to remove") + } + + if ok, hn := validateHostnames(args); !ok { + return errors.New(fmt.Sprintf("\"%s\" is not a valid hostname", hn)) + } + + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + if !Quiet { + fmt.Printf("Removing host(s) \"%s\"\n", strings.Join(args, " ")) + } + + RemoveHosts(args) + }, +} + +func RemoveHosts(hosts []string) { + + etcHosts.RemoveHosts(hosts) + + if DryRun { + fmt.Print(etcHosts.RenderHostsFile()) + return + } + + err := etcHosts.Save() + if err != nil { + fmt.Printf("Error: could not save %s. Reason: %s\n", etcHosts.WriteFilePath, err.Error()) + os.Exit(1) + } +} diff --git a/util/cmd/remove_ip.go b/util/cmd/remove_ip.go new file mode 100644 index 0000000..7263ae5 --- /dev/null +++ b/util/cmd/remove_ip.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" +) + +func init() { + removeCmd.AddCommand(removeIpCmd) +} + +var removeIpCmd = &cobra.Command{ + Use: "ip [IP] [IP] [IP]...", + Short: "Remove IP addresses from /etc/hosts", + Long: `Remove one or more IP addresses from /etc/hosts`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("the \"remove ip\" command requires at least one IP address to remove") + } + + if ok, ip := validateIPAddresses(args); !ok { + return errors.New(fmt.Sprintf("\"%s\" is not a valid ip address", ip)) + } + + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + if !Quiet { + fmt.Printf("Removing ip(s) \"%s\"\n", strings.Join(args, " ")) + } + + RemoveIPs(args) + }, +} + +func RemoveIPs(ips []string) { + + etcHosts.RemoveAddresses(ips) + + if DryRun { + fmt.Print(etcHosts.RenderHostsFile()) + return + } + + err := etcHosts.Save() + if err != nil { + fmt.Printf("Error: could not save %s. Reason: %s\n", etcHosts.WriteFilePath, err.Error()) + os.Exit(1) + } +} diff --git a/util/cmd/root.go b/util/cmd/root.go new file mode 100644 index 0000000..e85f522 --- /dev/null +++ b/util/cmd/root.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "fmt" + "net" + "os" + "regexp" + + "github.com/spf13/cobra" + "github.com/txn2/txeh" +) + +var rootCmd = &cobra.Command{ + Use: "txeh", + Short: "txeh is a /etc/hosts manager", + Long: ` _ _ +| |___ _____| |__ +| __\ \/ / _ \ '_ \ +| |_ > < __/ | | | + \__/_/\_\___|_| |_| v` + VERSION + ` + +Add, remove and re-associate hostname entries in your /etc/hosts file. +Read more including useage as a Go library at https://github.com/txn2/txeh`, + Run: func(cmd *cobra.Command, args []string) { + err := cmd.Help() + if err != nil { + fmt.Printf("Error: can not display help, reason: %s\n", err.Error()) + os.Exit(1) + } + + fmt.Println("Please specify a sub-command such as \"add\" or \"remove\"") + os.Exit(1) + }, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + initEtcHosts() + }, +} + +var Quiet bool +var HostsFileReadPath string +var HostsFileWritePath string +var DryRun bool + +var etcHosts *txeh.Hosts +var hostnameRegex *regexp.Regexp + +func init() { + rootCmd.PersistentFlags().BoolVarP(&DryRun, "dryrun", "d", false, "dry run, output to stdout (ignores quiet)") + rootCmd.PersistentFlags().BoolVarP(&Quiet, "quiet", "q", false, "no output") + rootCmd.PersistentFlags().StringVarP(&HostsFileReadPath, "read", "r", "", "(override) Path to read /etc/hosts file.") + rootCmd.PersistentFlags().StringVarP(&HostsFileWritePath, "write", "w", "", "(override) Path to write /etc/hosts file.") + + // validate hostnames (allow underscore for service records) + hostnameRegex = regexp.MustCompile(`^([A-Za-z]|[0-9]|-|_|\.)+$`) +} + +func validateIPAddresses(ips []string) (bool, string) { + for _, ip := range ips { + if validateIPAddress(ip) == false { + return false, ip + } + } + + return true, "" +} + +func validateIPAddress(ip string) bool { + + if net.ParseIP(ip) == nil { + return false + } + + return true +} + +func validateHostnames(hostnames []string) (bool, string) { + for _, hn := range hostnames { + if validateHostname(hn) != true { + return false, hn + } + } + + return true, "" +} + +func validateHostname(hostname string) bool { + return hostnameRegex.MatchString(hostname) +} + +func initEtcHosts() { + if HostsFileReadPath == "" && HostsFileWritePath == "" { + hosts, err := txeh.NewHostsDefault() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + etcHosts = hosts + return + } + + hosts, err := txeh.NewHosts(&txeh.HostsConfig{ + ReadFilePath: HostsFileReadPath, + WriteFilePath: HostsFileWritePath, + }) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + etcHosts = hosts +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } + return +} diff --git a/util/cmd/version.go b/util/cmd/version.go new file mode 100644 index 0000000..f233fcb --- /dev/null +++ b/util/cmd/version.go @@ -0,0 +1,22 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +const VERSION = "0.0.0" + +func init() { + rootCmd.AddCommand(versionCmd) +} + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of txeh", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("txeh Version %s", VERSION) + }, +} diff --git a/util/txeh.go b/util/txeh.go new file mode 100644 index 0000000..25a2c47 --- /dev/null +++ b/util/txeh.go @@ -0,0 +1,7 @@ +package main + +import "github.com/txn2/txeh/util/cmd" + +func main() { + cmd.Execute() +}