-
Notifications
You must be signed in to change notification settings - Fork 12
/
hosts.go
529 lines (448 loc) · 12.3 KB
/
hosts.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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
package hostsfile
import (
"bufio"
"bytes"
"fmt"
"net"
"os"
"path/filepath"
"sort"
"strings"
"github.com/asaskevich/govalidator"
"github.com/dimchansky/utfbom"
)
// Hosts represents hosts file with the path and parsed contents of each line
type Hosts struct {
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
}
// NewHosts return a new instance of Hosts using the default hosts file path.
func NewHosts() (*Hosts, error) {
osHostsFilePath := os.ExpandEnv(filepath.FromSlash(HostsFilePath))
if env, isset := os.LookupEnv("HOSTS_PATH"); isset && len(env) > 0 {
osHostsFilePath = os.ExpandEnv(filepath.FromSlash(env))
}
return NewCustomHosts(osHostsFilePath)
}
// NewCustomHosts return a new instance of Hosts using a custom hosts file path.
func NewCustomHosts(osHostsFilePath string) (*Hosts, error) {
hosts := &Hosts{
Path: osHostsFilePath,
ips: newLookup(),
hosts: newLookup(),
}
if err := hosts.Load(); err != nil {
return hosts, err
}
return hosts, nil
}
// String get a string of the contents of the contents to put in the hosts file
func (h *Hosts) String() string {
buf := new(bytes.Buffer)
for _, line := range h.Lines {
// 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()
}
// loadString is a helper function for testing but if we want to expose it somehow it's probably safe
func (h *Hosts) loadString(content string) error {
h.Clear()
rdr := strings.NewReader(content)
scanner := bufio.NewScanner(utfbom.SkipOnly(rdr))
for scanner.Scan() {
h.addLine(NewHostsLine(scanner.Text()))
}
return scanner.Err()
}
// IsWritable return true if hosts file is writable.
func (h *Hosts) IsWritable() bool {
file, err := os.OpenFile(h.Path, os.O_WRONLY, 0660)
if err != nil {
return false
}
defer file.Close()
return true
}
// Load the hosts file from the Path into Lines, called by NewHosts() and Hosts.Flush() and you should not need to call this yourself.
func (h *Hosts) Load() error {
file, err := os.Open(h.Path)
if err != nil {
return err
}
defer file.Close()
h.Clear() // reset the lines and lookups in case anything was previously set
scanner := bufio.NewScanner(utfbom.SkipOnly(file))
for scanner.Scan() {
h.addLine(NewHostsLine(scanner.Text()))
}
return scanner.Err()
}
// Flush writes to the file located at Path the contents of Lines in a hostsfile format
func (h *Hosts) Flush() error {
if err := h.preFlush(); err != nil {
return err
}
file, err := os.Create(h.Path)
if err != nil {
return err
}
defer file.Close()
w := bufio.NewWriter(file)
for _, line := range h.Lines {
if _, err := fmt.Fprintf(w, "%s%s", line.ToRaw(), eol); err != nil {
return err
}
}
if err := w.Flush(); err != nil {
return err
}
if err := h.postFlush(); err != nil {
return err
}
return h.Load()
}
// AddRaw takes a line from a hosts file and parses/adds the HostsLine
func (h *Hosts) AddRaw(raw ...string) error {
for _, r := range raw {
nl := NewHostsLine(r)
if nl.IP != "" && net.ParseIP(nl.IP) == nil {
return fmt.Errorf("%q is an invalid IP address", nl.IP)
}
for _, host := range nl.Hosts {
if !govalidator.IsDNSName(host) {
return fmt.Errorf("hostname is not a valid dns name: %s", host)
}
}
h.addLine(nl)
}
return nil
}
// Add an entry to the hosts file.
func (h *Hosts) Add(ip string, hosts ...string) error {
if net.ParseIP(ip) == nil {
return fmt.Errorf("%q is an invalid IP address", ip)
}
// remove hosts from other ips if it already exists
for _, host := range hosts {
for _, p := range h.hosts.get(host) {
if h.Lines[p].IP == ip {
continue
}
if err := h.Remove(h.Lines[p].IP, host); err != nil {
return err
}
}
}
position := h.ips.get(ip)
if len(position) == 0 {
h.addLine(HostsLine{
Raw: fmt.Sprintf("%s %s", ip, strings.Join(hosts, " ")),
IP: ip,
Hosts: hosts,
})
} else {
// add new host to the first one we find
loc := position[len(position)-1] // last element
hostsCopy := make([]string, len(h.Lines[loc].Hosts))
copy(hostsCopy, h.Lines[loc].Hosts)
for _, addHost := range hosts {
if h.Has(ip, addHost) {
continue // this combo already exists
}
if !govalidator.IsDNSName(addHost) {
return fmt.Errorf("hostname is not a valid dns name: %s", addHost)
}
hostsCopy = append(hostsCopy, addHost)
h.hosts.add(addHost, loc)
}
h.Lines[loc].Hosts = hostsCopy
h.Lines[loc].RegenRaw()
}
return nil
}
func (h *Hosts) Clear() {
h.Lines = []HostsLine{}
h.ips.reset()
h.hosts.reset()
}
// Clean merge duplicate ips and hosts per ip
func (h *Hosts) Clean() {
h.CombineDuplicateIPs()
h.RemoveDuplicateHosts()
h.SortHosts()
h.SortIPs()
h.HostsPerLine(HostsPerLine)
}
// Has return a bool if ip/host combo exists in the Lines
func (h *Hosts) Has(ip string, host string) bool {
ippos := h.ips.get(ip)
hostpos := h.hosts.get(host)
for _, pos := range ippos {
if itemInSliceInt(pos, hostpos) {
// if ip and host have matching lookup positions we have a combo match
return true
}
}
return false
}
// HasHostname return a bool if hostname in hosts file.
func (h *Hosts) HasHostname(host string) bool {
return len(h.hosts.get(host)) > 0
}
// Deprecated: HasIp will be replaced by HasIP
func (h *Hosts) HasIp(ip string) bool {
return h.HasIP(ip)
}
// HasIP will check if the ip exists
func (h *Hosts) HasIP(ip string) bool {
return len(h.ips.get(ip)) > 0
}
// Remove takes an ip and an optional host(s), if only an ip is passed the whole line is removed
// when the optional hosts param is passed it will remove only those specific hosts from that ip
func (h *Hosts) Remove(ip string, hosts ...string) error {
if net.ParseIP(ip) == nil {
return fmt.Errorf("%q is an invalid IP address", ip)
}
if len(hosts) == 0 {
return nil // no point in trying
}
lines := make([]HostsLine, len(h.Lines))
copy(lines, h.Lines)
h.Clear()
for _, line := range lines {
// add back all lines which were not the passed ip
if line.IP != ip {
h.addLine(line)
continue
}
var newHosts []string
for _, checkHost := range line.Hosts {
if !itemInSliceString(checkHost, hosts) {
newHosts = append(newHosts, checkHost)
}
}
// If hosts is empty, skip the line completely.
if len(newHosts) == 0 {
continue
}
// ip still has hosts, add it back
line.Hosts = newHosts
line.RegenRaw()
h.addLine(line)
}
return nil
}
// RemoveByHostname go through all lines and remove a hostname if it exists
func (h *Hosts) RemoveByHostname(host string) error {
restart := true
for restart {
restart = false
for _, p := range h.hosts.get(host) {
line := &h.Lines[p]
if len(line.Hosts) > 0 {
line.Hosts = removeFromSliceString(host, line.Hosts)
line.RegenRaw()
}
h.hosts.remove(host, p)
// cleanup the whole line if there remains an IP address
// without hostname/alias
if len(line.Hosts) == 0 {
h.removeByPosition(p)
// when an entry in the lines array is removed
// the range from hosts.get() above is
// outdated. Therefore, the whole procedure needs
// to restart over again
restart = true
break
}
}
}
h.reindex()
return nil
}
func (h *Hosts) RemoveByIP(ip string) {
pos := h.ips.get(ip)
for _, p := range pos {
h.removeByPosition(p)
}
}
// Deprecated: RemoveByIp this got refactored and wont return an error any more
// leaving it for stable api purposes, will be removed in a major release
func (h *Hosts) RemoveByIp(ip string) error {
h.RemoveByIP(ip)
return nil
}
// Deprecated: RemoveDuplicateIps deprecated will be deprecated, use Combine
func (h *Hosts) RemoveDuplicateIps() {
h.CombineDuplicateIPs()
}
// CombineDuplicateIPs finds all duplicate ips and combines all their hosts into one line
func (h *Hosts) CombineDuplicateIPs() {
ipCount := make(map[string]int)
for _, line := range h.Lines {
if line.IP == "" {
continue // ignore comments
}
ipCount[line.IP]++
}
for ip, count := range ipCount {
if count > 1 {
// todo: combine will rebuild lines and indexes, maybe rewrite to do the rebuild and call reindex?
h.combineIP(ip)
}
}
}
func (h *Hosts) combineIP(ip string) {
newLine := HostsLine{
IP: ip,
}
lines := make([]HostsLine, len(h.Lines))
copy(lines, h.Lines)
// clear the lines and position indexes to start over
h.Clear()
for _, line := range lines {
if line.IP == ip {
// if you find the ip combine it into newline
newLine.combine(line)
continue
}
// add everyone else
h.addLine(line)
}
// sort the hosts and add it to the end of Lines
newLine.SortHosts()
h.addLine(newLine)
}
// RemoveDuplicateHosts will check each line and remove hosts if they are the same
func (h *Hosts) RemoveDuplicateHosts() {
for pos := range h.Lines {
if h.Lines[pos].IsComment() {
continue // skip comments
}
h.Lines[pos].RemoveDuplicateHosts()
for _, host := range h.Lines[pos].Hosts {
h.hosts.remove(host, pos)
}
}
}
// SortHosts will go through each line and sort the hosts in alpha order
func (h *Hosts) SortHosts() {
for pos := range h.Lines {
h.Lines[pos].SortHosts()
}
}
// Deprecated: SortByIp switch to SortByIP
func (h *Hosts) SortByIp() {
h.SortIPs()
}
// SortByIP convert to net.IP and byte.Compare, maintains all comment only lines at the top
func (h *Hosts) SortIPs() {
// create a new list of unique ips, if dupe ips they will still get grouped together
uniqueIPs := make([]net.IP, 0, len(h.Lines))
unique := make(map[string]struct{})
for _, l := range h.Lines {
if _, ok := unique[l.IP]; !ok {
unique[l.IP] = struct{}{}
uniqueIPs = append(uniqueIPs, net.ParseIP(l.IP))
}
}
// sort the new unique list
sort.Slice(uniqueIPs, func(i, j int) bool {
return bytes.Compare(uniqueIPs[i], uniqueIPs[j]) < 0
})
// create a copy of the lines and Clear
lines := make([]HostsLine, len(h.Lines))
copy(lines, h.Lines)
// clear the lines and position indexes to start over
h.Clear()
// put all the comments back at the top
for _, l := range lines {
if l.IP == "" {
h.addLine(l)
}
}
// loop over the sorted ips and find their line and add it
for _, ip := range uniqueIPs {
for _, l := range lines {
if ip.String() == l.IP {
h.addLine(l) // no continue to group duplicate ips
}
}
}
}
// HostsPerLine checks all ips and if their host count is greater than count will split into multiple lines with max of count hosts per line
func (h *Hosts) HostsPerLine(count int) {
if count <= 0 {
return
}
// make a local copy
lines := make([]HostsLine, len(h.Lines))
copy(lines, h.Lines)
// clear the lines and position indexes to start over
h.Clear()
for ln, line := range lines {
if len(line.Hosts) <= count {
for _, host := range line.Hosts {
h.hosts.add(host, ln)
}
h.ips.add(line.IP, ln)
h.Lines = append(h.Lines, line)
continue
}
// i: index of the host, j: offset for line number
for i, j := 0, 0; i < len(line.Hosts); i, j = i+count, j+1 {
lineCopy := line
end := len(line.Hosts)
if end > i+count {
end = i + count
}
for _, host := range line.Hosts {
h.hosts.add(host, ln+j)
}
h.ips.add(line.IP, ln+j)
lineCopy.Hosts = line.Hosts[i:end]
lineCopy.RegenRaw()
h.Lines = append(h.Lines, lineCopy)
}
}
}
// addLine ill append a new HostsLine and add it to the indexes
func (h *Hosts) addLine(line HostsLine) {
h.Lines = append(h.Lines, line)
if line.IsComment() {
return // don't index comments
}
pos := len(h.Lines) - 1
h.ips.add(line.IP, pos)
for _, host := range line.Hosts {
h.hosts.add(host, pos)
}
}
// removeByPosition will drop a line located at pos and reindex all lookups
func (h *Hosts) removeByPosition(pos int) {
if pos == 0 && len(h.Lines) == 1 {
h.Clear()
return
}
h.Lines = append(h.Lines[:pos], h.Lines[pos+1:]...)
h.reindex()
}
// reindex will reset the internal position arrays for host/ips and rerun the add commands and should be run everytime
// a HostLine is removed. During the add process it's faster to just call the adds instead of reindex as it's more expensive.
func (h *Hosts) reindex() {
h.hosts.Lock()
h.hosts.l = make(map[string][]int)
h.hosts.Unlock()
h.ips.Lock()
h.ips.l = make(map[string][]int)
h.ips.Unlock()
for pos, line := range h.Lines {
h.ips.add(line.IP, pos)
for _, host := range line.Hosts {
h.hosts.add(host, pos)
}
}
}