diff --git a/README.md b/README.md index 57cb9bb..b393dc0 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,62 @@ -# Go library for working with a system's hostsfile -[![codecov](https://codecov.io/gh/goodhosts/hostsfile/branch/master/graph/badge.svg?token=BJQH16QQEH)](https://codecov.io/gh/goodhosts/hostsfile) -## Usage +# Go package for working with a system's hostsfile +[![codecov](https://codecov.io/gh/goodhosts/hostsfile/branch/main/graph/badge.svg?token=BJQH16QQEH)](https://codecov.io/gh/goodhosts/hostsfile) +[![Go Reference](https://pkg.go.dev/badge/github.com/goodhosts/hostsfile.svg)](https://pkg.go.dev/github.com/goodhosts/hostsfile) -Using system default hosts file +Reads the content of a file in the [hosts format](https://en.wikipedia.org/wiki/Hosts_(file)) into go structs for easy manipulation in go programs. When all changes are complete you can `Flush` the hosts file back to disk to save your changes. Supports an indexing system on both ips and hosts for quick management of large hosts files. + +## Simple Usage +Simple usage reading in your system's hosts file and adding an entry for the ip `192.168.1.1` and the host `my-hostname` + +```go +package main + +import ( + "log" + + "github.com/goodhosts/hostsfile" +) + +func main() { + hosts, err := hostsfile.NewHosts() + if err != nil { + log.Fatal(err.Error()) + } + if err := hosts.Add("192.168.1.1", "my-hostname"); err != nil { + log.Fatal(err.Error()) + } + if err := hosts.Flush(); err != nil { + log.Fatal(err.Error()) + } +} ``` -hfile, err := hostsfile.NewHosts() + +### Other Usage +Read in a hosts file from a custom location which is not the system default, this is useful for tests or systems with non-standard hosts file locations. +``` +hosts, err := hostsfile.NewCustomHosts("./my-custom-hostsfile") ``` -Using a custom hostsfile at a specific location +Use `Add` to put an ip and host combination in the hosts file ``` -hfile, err := hostsfile.NewCustomHosts("./my-custom-hostsfile") +err := hosts.Add("192.168.1.1", "my-hostname") ``` -Add an ip entry with it's hosts +`Add` is variadic and can take multiple hosts to add for the same ip ``` -err := hfile.Add("192.168.1.1", "my-hostname", "another-hostname") +err := hosts.Add("192.168.1.1", "my-hostname", "another-hostname") ``` -Remove an ip/host combination +Use `Remove` to drop an ip and host combination from the hosts file ``` -err := hfile.Remove("192.168.1.1", "another-hostname") +err := hosts.Remove("192.168.1.1", "my-hostname") ``` -Flush the hostfile changes back to disk +`Remove` is variadic and can take multiple hosts to remove from the same ip ``` -err := hfile.Flush() +err := hosts.Remove("192.168.1.1", "my-hostname", "another-hostname") ``` -# Full API +Flush the hosts file changes back to disk +``` +err := hosts.Flush() ``` -type Hosts -func NewCustomHosts(osHostsFilePath string) (*Hosts, error) - func NewHosts() (*Hosts, error) - func (h *Hosts) Add(ip string, hosts ...string) error - func (h *Hosts) AddRaw(raw ...string) error - func (h *Hosts) Clean() - func (h *Hosts) Clear() - func (h *Hosts) Flush() error - func (h *Hosts) Has(ip string, host string) bool - func (h *Hosts) HasHostname(host string) bool - func (h *Hosts) HasIp(ip string) bool - func (h *Hosts) HostsPerLine(count int) - func (h *Hosts) IsWritable() bool - func (h *Hosts) Load() error - func (h *Hosts) Remove(ip string, hosts ...string) error - func (h *Hosts) RemoveByHostname(host string) error - func (h *Hosts) RemoveByIp(ip string) error - func (h *Hosts) RemoveDuplicateHosts() - func (h *Hosts) RemoveDuplicateIps() - func (h *Hosts) SortByIp() - func (h *Hosts) SortHosts() -type HostsLine -func NewHostsLine(raw string) HostsLine - func (l *HostsLine) Combine(hostline HostsLine) - func (l *HostsLine) HasComment() bool - func (l *HostsLine) IsComment() bool - func (l *HostsLine) IsMalformed() bool - func (l *HostsLine) IsValid() bool - func (l *HostsLine) RegenRaw() - func (l *HostsLine) RemoveDuplicateHosts() - func (l *HostsLine) SortHosts() - func (l *HostsLine) ToRaw() string -``` \ No newline at end of file diff --git a/hosts.go b/hosts.go index 2de187d..b15593a 100644 --- a/hosts.go +++ b/hosts.go @@ -14,9 +14,11 @@ import ( "github.com/dimchansky/utfbom" ) +// Hosts represents hosts file with the path and parsed contents of each line type Hosts struct { - Path string - Lines []HostsLine + Path string // Path to the location of the hosts file that will be loaded/flushed + Lines []HostsLine // Slice containing all the lines parsed from the hosts file + ips lookup hosts lookup } @@ -51,10 +53,8 @@ func NewCustomHosts(osHostsFilePath string) (*Hosts, error) { func (h *Hosts) String() string { buf := new(bytes.Buffer) for _, line := range h.Lines { - if _, err := fmt.Fprintf(buf, "%s%s", line.ToRaw(), eol); err != nil { - // unlikely we will error during writing to a string buffer? maybe we dont need to do anything here - return err.Error() - } + // bytes buffers doesn't actually throw errors but the io.Writer interface requires it + fmt.Fprintf(buf, "%s%s", line.ToRaw(), eol) } return buf.String() } @@ -178,16 +178,12 @@ func (h *Hosts) Add(ip string, hosts ...string) error { hostsCopy := h.Lines[position[0]].Hosts for _, addHost := range hosts { if h.Has(ip, addHost) { - // this combo already exists - continue + continue // this combo already exists } if !govalidator.IsDNSName(addHost) { return fmt.Errorf("hostname is not a valid dns name: %s", addHost) } - if itemInSliceString(addHost, hostsCopy) { - continue // host exists for ip already - } hostsCopy = append(hostsCopy, addHost) h.hosts.add(addHost, position[0]) @@ -367,7 +363,7 @@ func (h *Hosts) combineIP(ip string) { for _, line := range lines { if line.IP == ip { // if you find the ip combine it into newline - newLine.Combine(line) + newLine.combine(line) continue } // add everyone else diff --git a/hosts_test.go b/hosts_test.go index 58e1310..3b142db 100644 --- a/hosts_test.go +++ b/hosts_test.go @@ -2,10 +2,12 @@ package hostsfile import ( "errors" + "fmt" "log" "math/rand" "os" "path/filepath" + "strings" "sync" "testing" @@ -25,8 +27,8 @@ func randomString(n int) string { func newHosts() *Hosts { return &Hosts{ - ips: lookup{l: make(map[string][]int)}, - hosts: lookup{l: make(map[string][]int)}, + ips: newLookup(), + hosts: newLookup(), } } @@ -469,11 +471,18 @@ func TestHosts_Clean(t *testing.T) { func TestHosts_Add(t *testing.T) { hosts := newHosts() - assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) // valid use with variatic args - assert.Error(t, assert.AnError, hosts.Add("127.0.0.2", "host11 host12 host13 host14 host15 host16 host17 host18 hosts19 hosts20")) // invalid use + + assert.Error(t, hosts.Add("badip", "hosts1")) + assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) // valid use with variatic args assert.Len(t, hosts.Lines, 1) - assert.Nil(t, hosts.Add("127.0.0.3", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) + assert.Error(t, hosts.Add("127.0.0.2", "host11 host12 host13 host14 host15 host16 host17 host18 hosts19 hosts20")) // invalid use + assert.Len(t, hosts.Lines, 1) + assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) assert.Len(t, hosts.Lines, 1) + + // add the same hosts twice (should be noop with nothing new) + assert.Nil(t, hosts.Add("127.0.0.3", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) + assert.Error(t, assert.AnError, hosts.Add("127.0.0.3", "invalid hostname")) assert.Error(t, assert.AnError, hosts.Add("127.0.0.3", ".invalid*hostname")) @@ -510,6 +519,7 @@ func TestHosts_Add(t *testing.T) { // add a new ip with 10 hosts, should remove first ip assert.Nil(t, hosts.Add("127.0.0.3", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) assert.False(t, hosts.HasIP("127.0.0.2")) + assert.False(t, hosts.HasIp("127.0.0.2")) assert.Len(t, hosts.Lines, 1) assert.Len(t, hosts.hosts.l, 10) assert.Len(t, hosts.ips.l, 1) @@ -525,6 +535,22 @@ func TestHosts_Add(t *testing.T) { assert.Equal(t, expectedLines, hosts.Lines) } +func TestHosts_AddRaw(t *testing.T) { + hosts := newHosts() + + assert.Nil(t, hosts.AddRaw("127.0.0.1 yadda")) + assert.Len(t, hosts.Lines, 1) + + assert.Nil(t, hosts.AddRaw("127.0.0.2 nada")) + assert.Len(t, hosts.Lines, 2) + + assert.Nil(t, hosts.AddRaw("127.0.0.3 host1", "127.0.0.4 host2")) + assert.Len(t, hosts.Lines, 4) + + assert.Error(t, hosts.AddRaw("badip host1")) // fail ip parse + assert.Error(t, hosts.AddRaw("127.0.0.1 host1%")) // fail host DNS validation +} + func TestHosts_HostsPerLine(t *testing.T) { hosts := newHosts() assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) @@ -563,7 +589,26 @@ func TestHosts_HostsPerLine(t *testing.T) { hosts.Clear() assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) hosts.HostsPerLine(8) + assert.Len(t, hosts.Lines, 2) + assert.Len(t, hosts.ips.l, 1) + assert.Len(t, hosts.hosts.l, 10) + + hosts.HostsPerLine(0) // noop + assert.Len(t, hosts.Lines, 2) + assert.Len(t, hosts.ips.l, 1) + assert.Len(t, hosts.hosts.l, 10) + assert.Nil(t, hosts.Add("127.0.0.2", "host1", "host2", "host3", "host4", "host5", "host6", "host7", "host8", "host9", "hosts10")) + +} + +func BenchmarkHosts_Add(b *testing.B) { + for _, c := range []int{10000, 25000, 50000, 100000, 250000, 500000} { + b.Run(fmt.Sprintf("%d", c), func(b *testing.B) { + benchmarkHosts_Add(c, b) + // mem() + }) + } } func BenchmarkHosts_Add10k(b *testing.B) { @@ -583,7 +628,14 @@ func BenchmarkHosts_Add250k(b *testing.B) { } func benchmarkHosts_Add(c int, b *testing.B) { - hosts, err := NewCustomHosts("hostsfile") + fp := "hostsfile" + f, err := os.Create(fp) + assert.Nil(b, err) + defer func() { + assert.Nil(b, f.Close()) + assert.Nil(b, os.Remove(fp)) + }() + hosts, err := NewCustomHosts(fp) assert.Nil(b, err) for i := 0; i < c; i++ { assert.Nil(b, hosts.Add(fake.IPv4(), randomString(63))) @@ -606,10 +658,14 @@ func BenchmarkHosts_Flush500k(b *testing.B) { benchmarkHosts_Flush(50, b) } -// benchmarks flushing a hostsfile and confirms the hashmap lookup for ips/hosts is thread save via mutex + locking +// benchmarks flushing a hostsfile and confirms the hashmap lookup for ips/hosts is thread safe via mutex + locking func benchmarkHosts_Flush(c int, b *testing.B) { _, err := os.Create("hostsfile") assert.Nil(b, err) + defer func() { + assert.Nil(b, os.Remove("hostsfile")) + }() + hosts, err := NewCustomHosts("hostsfile") assert.Nil(b, err) @@ -626,7 +682,6 @@ func benchmarkHosts_Flush(c int, b *testing.B) { wg.Wait() assert.Nil(b, hosts.Flush()) - assert.Nil(b, os.Remove("hostsfile")) } func TestHosts_Flush(t *testing.T) { @@ -696,9 +751,45 @@ func TestHosts_RemoveDuplicateHosts(t *testing.T) { func TestHosts_CombineDuplicateIPs(t *testing.T) { hosts := newHosts() - assert.Nil(t, hosts.loadString(`127.0.0.1 test1 test1 test2 test2`+eol+`127.0.0.1 test1 test1 test2 test2`+eol)) + assert.Nil(t, hosts.loadString(`# comment`+eol+`127.0.0.1 test1 test1 test2 test2`+eol+`127.0.0.1 test1 test1 test2 test2`+eol)) hosts.CombineDuplicateIPs() - assert.Len(t, hosts.Lines, 1) - assert.Equal(t, "127.0.0.1 test1 test1 test1 test1 test2 test2 test2 test2"+eol, hosts.String()) + assert.Len(t, hosts.Lines, 2) + assert.Equal(t, "# comment"+eol+"127.0.0.1 test1 test1 test1 test1 test2 test2 test2 test2"+eol, hosts.String()) + + // deprecated + hosts = newHosts() + assert.Nil(t, hosts.loadString(`# comment`+eol+`127.0.0.1 test1 test1 test2 test2`+eol+`127.0.0.1 test1 test1 test2 test2`+eol)) + hosts.RemoveDuplicateIps() + assert.Len(t, hosts.Lines, 2) + assert.Equal(t, "# comment"+eol+"127.0.0.1 test1 test1 test1 test1 test2 test2 test2 test2"+eol, hosts.String()) +} + +func TestHosts_SortIPs(t *testing.T) { + hosts := newHosts() + assert.Nil(t, hosts.loadString(`# comment `+eol+`127.0.0.3 host3`+eol+`127.0.0.2 host2`+eol+`127.0.0.1 host1`+eol)) + + hosts.SortIPs() + assert.Len(t, hosts.Lines, 4) + assert.Equal(t, strings.Join([]string{ + "# comment ", + "127.0.0.1 host1", + "127.0.0.2 host2", + "127.0.0.3 host3", + "", + }, eol), hosts.String()) + + // deprecated + hosts = newHosts() + assert.Nil(t, hosts.loadString(`# comment `+eol+`127.0.0.3 host3`+eol+`127.0.0.2 host2`+eol+`127.0.0.1 host1`+eol)) + + hosts.SortByIp() + assert.Len(t, hosts.Lines, 4) + assert.Equal(t, strings.Join([]string{ + "# comment ", + "127.0.0.1 host1", + "127.0.0.2 host2", + "127.0.0.3 host3", + "", + }, eol), hosts.String()) } diff --git a/hostsline.go b/hostsline.go index 247503b..92a6e2b 100644 --- a/hostsline.go +++ b/hostsline.go @@ -7,17 +7,19 @@ import ( "strings" ) +// HostsLine represents a line of the hosts file after being parsed into their respective parts type HostsLine struct { - IP string - Hosts []string - Raw string - Err error - Comment string + IP string // IP found at the beginning of the line + Hosts []string // Hosts split into a slice on the space char + Comment string // Contents of everything after the comment char in the line + + Raw string // Raw contents of the line as parsed in or updated after changes + Err error // Used for error checking during parsing } const commentChar string = "#" -// NewHostsLine return a new instance of HostsLine. +// NewHostsLine takes a raw line as a string and parses it into a new instance of HostsLine e.g. "192.168.1.1 host1 host2 # comments" func NewHostsLine(raw string) HostsLine { output := HostsLine{Raw: raw} @@ -47,6 +49,12 @@ func NewHostsLine(raw string) HostsLine { return output } +// String to make HostsLine a fmt.Stringer +func (l *HostsLine) String() string { + return l.ToRaw() +} + +// ToRaw returns the HostsLine's contents as a raw string func (l *HostsLine) ToRaw() string { var comment string if l.IsComment() { //Whole line is comment @@ -77,7 +85,11 @@ func (l *HostsLine) RemoveDuplicateHosts() { l.RegenRaw() } +// Deprecated: will be made internal, combines the hosts and comments of two lines together, func (l *HostsLine) Combine(hostline HostsLine) { + l.combine(hostline) +} +func (l *HostsLine) combine(hostline HostsLine) { l.Hosts = append(l.Hosts, hostline.Hosts...) if l.Comment == "" { l.Comment = hostline.Comment