]> Sergey Matveev's repositories - btrtrc.git/blob - iplist/iplist.go
Some utils moved to missinggo
[btrtrc.git] / iplist / iplist.go
1 // Package iplist handles the P2P Plaintext Format described by
2 // https://en.wikipedia.org/wiki/PeerGuardian#P2P_plaintext_format.
3 package iplist
4
5 import (
6         "bufio"
7         "bytes"
8         "errors"
9         "fmt"
10         "io"
11         "net"
12         "sort"
13 )
14
15 type IPList struct {
16         ranges []Range
17 }
18
19 type Range struct {
20         First, Last net.IP
21         Description string
22 }
23
24 func (r *Range) String() string {
25         return fmt.Sprintf("%s-%s (%s)", r.First, r.Last, r.Description)
26 }
27
28 // Create a new IP list. The given ranges must already sorted by the lower
29 // bound IP in each range. Behaviour is undefined for lists of overlapping
30 // ranges.
31 func New(initSorted []Range) *IPList {
32         return &IPList{
33                 ranges: initSorted,
34         }
35 }
36
37 func (me *IPList) NumRanges() int {
38         if me == nil {
39                 return 0
40         }
41         return len(me.ranges)
42 }
43
44 // Return the range the given IP is in. Returns nil if no range is found.
45 func (me *IPList) Lookup(ip net.IP) (r *Range) {
46         if me == nil {
47                 return nil
48         }
49         // TODO: Perhaps all addresses should be converted to IPv6, if the future
50         // of IP is to always be backwards compatible. But this will cost 4x the
51         // memory for IPv4 addresses?
52         v4 := ip.To4()
53         if v4 != nil {
54                 r = me.lookup(v4)
55                 if r != nil {
56                         return
57                 }
58         }
59         v6 := ip.To16()
60         if v6 != nil {
61                 return me.lookup(v6)
62         }
63         if v4 == nil && v6 == nil {
64                 return &Range{
65                         Description: fmt.Sprintf("unsupported IP: %s", ip),
66                 }
67         }
68         return nil
69 }
70
71 // Return the range the given IP is in. Returns nil if no range is found.
72 func (me *IPList) lookup(ip net.IP) (r *Range) {
73         // Find the index of the first range for which the following range exceeds
74         // it.
75         i := sort.Search(len(me.ranges), func(i int) bool {
76                 if i+1 >= len(me.ranges) {
77                         return true
78                 }
79                 return bytes.Compare(ip, me.ranges[i+1].First) < 0
80         })
81         if i == len(me.ranges) {
82                 return
83         }
84         r = &me.ranges[i]
85         if bytes.Compare(ip, r.First) < 0 || bytes.Compare(ip, r.Last) > 0 {
86                 r = nil
87         }
88         return
89 }
90
91 func minifyIP(ip *net.IP) {
92         v4 := ip.To4()
93         if v4 != nil {
94                 *ip = append(make([]byte, 0, 4), v4...)
95         }
96 }
97
98 // Parse a line of the PeerGuardian Text Lists (P2P) Format. Returns !ok but
99 // no error if a line doesn't contain a range but isn't erroneous, such as
100 // comment and blank lines.
101 func ParseBlocklistP2PLine(l []byte) (r Range, ok bool, err error) {
102         l = bytes.TrimSpace(l)
103         if len(l) == 0 || bytes.HasPrefix(l, []byte("#")) {
104                 return
105         }
106         // TODO: Check this when IPv6 blocklists are available.
107         colon := bytes.LastIndexAny(l, ":")
108         if colon == -1 {
109                 err = errors.New("missing colon")
110                 return
111         }
112         hyphen := bytes.IndexByte(l[colon+1:], '-')
113         if hyphen == -1 {
114                 err = errors.New("missing hyphen")
115                 return
116         }
117         hyphen += colon + 1
118         r.Description = string(l[:colon])
119         r.First = net.ParseIP(string(l[colon+1 : hyphen]))
120         minifyIP(&r.First)
121         r.Last = net.ParseIP(string(l[hyphen+1:]))
122         minifyIP(&r.Last)
123         if r.First == nil || r.Last == nil || len(r.First) != len(r.Last) {
124                 err = errors.New("bad IP range")
125                 return
126         }
127         ok = true
128         return
129 }
130
131 // Creates an IPList from a line-delimited P2P Plaintext file.
132 func NewFromReader(f io.Reader) (ret *IPList, err error) {
133         var ranges []Range
134         // There's a lot of similar descriptions, so we maintain a pool and reuse
135         // them to reduce memory overhead.
136         uniqStrs := make(map[string]string)
137         scanner := bufio.NewScanner(f)
138         lineNum := 1
139         for scanner.Scan() {
140                 r, ok, lineErr := ParseBlocklistP2PLine(scanner.Bytes())
141                 if lineErr != nil {
142                         err = fmt.Errorf("error parsing line %d: %s", lineNum, lineErr)
143                         return
144                 }
145                 lineNum++
146                 if !ok {
147                         continue
148                 }
149                 if s, ok := uniqStrs[r.Description]; ok {
150                         r.Description = s
151                 } else {
152                         uniqStrs[r.Description] = r.Description
153                 }
154                 ranges = append(ranges, r)
155         }
156         err = scanner.Err()
157         if err != nil {
158                 return
159         }
160         ret = New(ranges)
161         return
162 }