-
-
Notifications
You must be signed in to change notification settings - Fork 274
/
index.go
288 lines (234 loc) Β· 7.51 KB
/
index.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
package filterlists
import (
"errors"
"fmt"
"os"
"strings"
"sync"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/formats/dsd"
"github.com/safing/portbase/log"
"github.com/safing/portbase/updater"
"github.com/safing/portmaster/service/updates"
)
// the following definitions are copied from the intelhub repository
// and stripped down to only include data required by portmaster.
// Category is used to group different list sources by the type
// of entity they are blocking. Categories may be nested using
// the Parent field.
type Category struct {
// ID is a unique ID for the category. For sub-categories
// this ID must be used in the Parent field of any directly
// nesteded categories.
ID string `json:"id"`
// Parent may hold the ID of another category. If set, this
// category is made a sub-category of it's parent.
Parent string `json:"parent,omitempty"`
// Name is a human readable name for the category and can
// be used in user interfaces.
Name string `json:"name"`
// Description is a human readable description that may be
// displayed in user interfaces.
Description string `json:"description,omitempty"`
}
// Source defines an external filterlists source.
type Source struct {
// ID is a unique ID for the source. Entities always reference the
// sources they have been observed in using this ID. Refer to the
// Entry struct for more information.
ID string `json:"id"`
// Name is a human readable name for the source and can be used
// in user interfaces.
Name string `json:"name"`
// Description may hold a human readable description for the source.
// It may be used in user interfaces.
Description string `json:"description"`
// Type describes the type of entities the source provides. Refer
// to the Type definition for more information and well-known types.
Type string `json:"type"`
// URL points to the filterlists file.
URL string `json:"url"`
// Category holds the unique ID of a category the source belongs to. Since
// categories can be nested the source is automatically part of all categories
// in the hierarchy. Refer to the Category struct for more information.
Category string `json:"category"`
// Website may holds the URL of the source maintainers website.
Website string `json:"website,omitempty"`
// License holds the license that is used for the source.
License string `json:"license"`
// Contribute may hold an opaque string that informs a user on how to
// contribute to the source. This may be a URL or mail address.
Contribute string `json:"contribute"`
}
// ListIndexFile describes the structure of the released list
// index file.
type ListIndexFile struct {
record.Base
sync.RWMutex
Version string `json:"version"`
SchemaVersion string `json:"schemaVersion"`
Categories []Category `json:"categories"`
Sources []Source `json:"sources"`
}
func (index *ListIndexFile) getCategorySources(id string) []string {
ids := make(map[string]struct{})
// find all sources that match against cat
for _, s := range index.Sources {
if s.Category == id {
ids[s.ID] = struct{}{}
}
}
// find all child-categories recursing into getCategorySources.
for _, c := range index.Categories {
if c.Parent == id {
for _, sid := range index.getCategorySources(c.ID) {
ids[sid] = struct{}{}
}
}
}
return mapKeys(ids)
}
func (index *ListIndexFile) getSourcesMatching(id string) []string {
// if id is already a source ID we just return it
for _, s := range index.Sources {
if s.ID == id {
return []string{s.ID}
}
}
// otherwise we need to check the category tree
return index.getCategorySources(id)
}
func (index *ListIndexFile) getDistictSourceIDs(ids ...string) []string {
index.RLock()
defer index.RUnlock()
distinctIDs := make(map[string]struct{})
for _, id := range ids {
for _, sid := range index.getSourcesMatching(id) {
distinctIDs[sid] = struct{}{}
}
}
return mapKeys(distinctIDs)
}
func getListIndexFromCache() (*ListIndexFile, error) {
r, err := cache.Get(filterListIndexKey)
if err != nil {
return nil, err
}
var index *ListIndexFile
if r.IsWrapped() {
index = new(ListIndexFile)
if err := record.Unwrap(r, index); err != nil {
return nil, err
}
} else {
var ok bool
index, ok = r.(*ListIndexFile)
if !ok {
return nil, fmt.Errorf("invalid type, expected ListIndexFile but got %T", r)
}
}
return index, nil
}
var (
// listIndexUpdate must only be used by updateListIndex.
listIndexUpdate *updater.File
listIndexUpdateLock sync.Mutex
)
func updateListIndex() error {
listIndexUpdateLock.Lock()
defer listIndexUpdateLock.Unlock()
// Check if an update is needed.
switch {
case listIndexUpdate == nil:
// This is the first time this function is run, get updater file for index.
var err error
listIndexUpdate, err = updates.GetFile(listIndexFilePath)
if err != nil {
return err
}
// Check if the version in the cache is current.
index, err := getListIndexFromCache()
switch {
case errors.Is(err, database.ErrNotFound):
log.Info("filterlists: index not in cache, starting update")
case err != nil:
log.Warningf("filterlists: failed to load index from cache, starting update: %s", err)
case !listIndexUpdate.EqualsVersion(strings.TrimPrefix(index.Version, "v")):
log.Infof(
"filterlists: index from cache is outdated, starting update (%s != %s)",
strings.TrimPrefix(index.Version, "v"),
listIndexUpdate.Version(),
)
default:
// List is in cache and current, there is nothing to do.
log.Debug("filterlists: index is up to date")
// Update the unbreak filter list IDs on initial load.
updateUnbreakFilterListIDs()
return nil
}
case listIndexUpdate.UpgradeAvailable():
log.Info("filterlists: index update available, starting update")
default:
// Index is loaded and no update is available, there is nothing to do.
return nil
}
// Update list index from updates.
blob, err := os.ReadFile(listIndexUpdate.Path())
if err != nil {
return err
}
index := &ListIndexFile{}
_, err = dsd.Load(blob, index)
if err != nil {
return err
}
index.SetKey(filterListIndexKey)
if err := cache.Put(index); err != nil {
return err
}
log.Debugf("intel/filterlists: updated list index in cache to %s", index.Version)
// Update the unbreak filter list IDs after an update.
updateUnbreakFilterListIDs()
return nil
}
// ResolveListIDs resolves a slice of source or category IDs into
// a slice of distinct source IDs.
func ResolveListIDs(ids []string) ([]string, error) {
index, err := getListIndexFromCache()
if err != nil {
if errors.Is(err, database.ErrNotFound) {
if err := updateListIndex(); err != nil {
return nil, err
}
// retry resolving IDs
return ResolveListIDs(ids)
}
log.Errorf("failed to resolved ids %v: %s", ids, err)
return nil, err
}
resolved := index.getDistictSourceIDs(ids...)
log.Debugf("intel/filterlists: resolved ids %v to %v", ids, resolved)
return resolved, nil
}
var (
unbreakCategoryIDs = []string{"UNBREAK"}
unbreakIDs []string
unbreakIDsLock sync.Mutex
)
// GetUnbreakFilterListIDs returns the resolved list of all unbreak filter lists.
func GetUnbreakFilterListIDs() []string {
unbreakIDsLock.Lock()
defer unbreakIDsLock.Unlock()
return unbreakIDs
}
func updateUnbreakFilterListIDs() {
unbreakIDsLock.Lock()
defer unbreakIDsLock.Unlock()
resolvedIDs, err := ResolveListIDs(unbreakCategoryIDs)
if err != nil {
log.Warningf("filter: failed to resolve unbreak filter list IDs: %s", err)
} else {
unbreakIDs = resolvedIDs
}
}