-
Notifications
You must be signed in to change notification settings - Fork 18.6k
/
etchosts.go
201 lines (168 loc) · 4.1 KB
/
etchosts.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
package etchosts
import (
"bufio"
"bytes"
"fmt"
"io"
"net/netip"
"os"
"regexp"
"sync"
)
// Record Structure for a single host record
type Record struct {
Hosts string
IP string
}
// WriteTo writes record to file and returns bytes written or error
func (r Record) WriteTo(w io.Writer) (int64, error) {
n, err := fmt.Fprintf(w, "%s\t%s\n", r.IP, r.Hosts)
return int64(n), err
}
var (
// Default hosts config records slice
defaultContentIPv4 = []Record{
{Hosts: "localhost", IP: "127.0.0.1"},
}
defaultContentIPv6 = []Record{
{Hosts: "localhost ip6-localhost ip6-loopback", IP: "::1"},
{Hosts: "ip6-localnet", IP: "fe00::0"},
{Hosts: "ip6-mcastprefix", IP: "ff00::0"},
{Hosts: "ip6-allnodes", IP: "ff02::1"},
{Hosts: "ip6-allrouters", IP: "ff02::2"},
}
// A cache of path level locks for synchronizing /etc/hosts
// updates on a file level
pathMap = make(map[string]*sync.Mutex)
// A package level mutex to synchronize the cache itself
pathMutex sync.Mutex
)
func pathLock(path string) func() {
pathMutex.Lock()
defer pathMutex.Unlock()
pl, ok := pathMap[path]
if !ok {
pl = &sync.Mutex{}
pathMap[path] = pl
}
pl.Lock()
return func() {
pl.Unlock()
}
}
// Drop drops the path string from the path cache
func Drop(path string) {
pathMutex.Lock()
defer pathMutex.Unlock()
delete(pathMap, path)
}
// Build function
// path is path to host file string required
// extraContent is an array of extra host records.
func Build(path string, extraContent []Record) error {
return build(path, defaultContentIPv4, defaultContentIPv6, extraContent)
}
// BuildNoIPv6 is the same as Build, but will not include IPv6 entries.
func BuildNoIPv6(path string, extraContent []Record) error {
var ipv4ExtraContent []Record
for _, rec := range extraContent {
addr, err := netip.ParseAddr(rec.IP)
if err != nil || !addr.Is6() {
ipv4ExtraContent = append(ipv4ExtraContent, rec)
}
}
return build(path, defaultContentIPv4, ipv4ExtraContent)
}
func build(path string, contents ...[]Record) error {
defer pathLock(path)()
buf := bytes.NewBuffer(nil)
// Write content from function arguments
for _, content := range contents {
for _, c := range content {
if _, err := c.WriteTo(buf); err != nil {
return err
}
}
}
return os.WriteFile(path, buf.Bytes(), 0o644)
}
// Add adds an arbitrary number of Records to an already existing /etc/hosts file
func Add(path string, recs []Record) error {
defer pathLock(path)()
if len(recs) == 0 {
return nil
}
b, err := mergeRecords(path, recs)
if err != nil {
return err
}
return os.WriteFile(path, b, 0o644)
}
func mergeRecords(path string, recs []Record) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
content := bytes.NewBuffer(nil)
if _, err := content.ReadFrom(f); err != nil {
return nil, err
}
for _, r := range recs {
if _, err := r.WriteTo(content); err != nil {
return nil, err
}
}
return content.Bytes(), nil
}
// Delete deletes an arbitrary number of Records already existing in /etc/hosts file
func Delete(path string, recs []Record) error {
defer pathLock(path)()
if len(recs) == 0 {
return nil
}
old, err := os.Open(path)
if err != nil {
return err
}
var buf bytes.Buffer
s := bufio.NewScanner(old)
eol := []byte{'\n'}
loop:
for s.Scan() {
b := s.Bytes()
if len(b) == 0 {
continue
}
if b[0] == '#' {
buf.Write(b)
buf.Write(eol)
continue
}
for _, r := range recs {
if bytes.HasSuffix(b, []byte("\t"+r.Hosts)) {
continue loop
}
}
buf.Write(b)
buf.Write(eol)
}
old.Close()
if err := s.Err(); err != nil {
return err
}
return os.WriteFile(path, buf.Bytes(), 0o644)
}
// Update all IP addresses where hostname matches.
// path is path to host file
// IP is new IP address
// hostname is hostname to search for to replace IP
func Update(path, IP, hostname string) error {
defer pathLock(path)()
old, err := os.ReadFile(path)
if err != nil {
return err
}
re := regexp.MustCompile(fmt.Sprintf("(\\S*)(\\t%s)(\\s|\\.)", regexp.QuoteMeta(hostname)))
return os.WriteFile(path, re.ReplaceAll(old, []byte(IP+"$2"+"$3")), 0o644)
}