-
Notifications
You must be signed in to change notification settings - Fork 0
/
features.go
353 lines (317 loc) · 10.6 KB
/
features.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
package config
import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/mitchellh/mapstructure"
"github.com/blang/semver"
"github.com/getlantern/errors"
)
const (
FeatureAuth = "auth"
FeatureProxyBench = "proxybench"
FeaturePingProxies = "pingproxies"
FeatureTrafficLog = "trafficlog"
FeatureNoBorda = "noborda"
FeatureProbeProxies = "probeproxies"
FeatureShortcut = "shortcut"
FeatureDetour = "detour"
FeatureNoHTTPSEverywhere = "nohttpseverywhere"
FeatureReplica = "replica"
FeatureProxyWhitelistedOnly = "proxywhitelistedonly"
FeatureTrackYouTube = "trackyoutube"
FeatureGoogleSearchAds = "googlesearchads"
FeatureYinbiWallet = "yinbiwallet"
FeatureYinbi = "yinbi"
FeatureGoogleAnalytics = "googleanalytics"
FeatureMatomo = "matomo"
FeatureChat = "chat"
)
var (
// to have stable calculation of fraction until the client restarts.
randomFloat = rand.Float64()
errAbsentOption = errors.New("option is absent")
errMalformedOption = errors.New("malformed option")
)
// FeatureOptions is an interface implemented by all feature options
type FeatureOptions interface {
fromMap(map[string]interface{}) error
}
type ReplicaOptionsRoot struct {
// This is the default.
ReplicaOptions `mapstructure:",squash"`
// Options tailored to country. This could be used to pattern match any arbitrary string really.
// mapstructure should ignore the field name.
ByCountry map[string]ReplicaOptions `mapstructure:",remain"`
// Deprecated. An unmatched country uses the embedded ReplicaOptions.ReplicaRustEndpoint.
// Removing this will break unmarshalling config.
ReplicaRustDefaultEndpoint string
// Deprecated. Use ByCountry.ReplicaRustEndpoint.
ReplicaRustEndpoints map[string]string
}
func (ro *ReplicaOptionsRoot) fromMap(m map[string]interface{}) error {
return mapstructure.Decode(m, ro)
}
type ReplicaOptions struct {
// Use infohash and old-style prefixing simultaneously for now. Later, the old-style can be removed.
WebseedBaseUrls []string
Trackers []string
StaticPeerAddrs []string
// Merged with the webseed URLs when the metadata and data buckets are merged.
MetadataBaseUrls []string
// The replica-rust endpoint to use. There's only one because object uploads and ownership are
// fixed to a specific bucket, and replica-rust endpoints are 1:1 with a bucket.
ReplicaRustEndpoint string
// A set of info hashes (20 bytes, hex-encoded) to which proxies should announce themselves.
ProxyAnnounceTargets []string
// A set of info hashes where p2p-proxy peers can be found.
ProxyPeerInfoHashes []string
}
func (ro *ReplicaOptions) GetWebseedBaseUrls() []string {
return ro.WebseedBaseUrls
}
func (ro *ReplicaOptions) GetTrackers() []string {
return ro.Trackers
}
func (ro *ReplicaOptions) GetStaticPeerAddrs() []string {
return ro.StaticPeerAddrs
}
func (ro *ReplicaOptions) GetMetadataBaseUrls() []string {
return ro.MetadataBaseUrls
}
func (ro *ReplicaOptions) GetReplicaRustEndpoint() string {
return ro.ReplicaRustEndpoint
}
type GoogleSearchAdsOptions struct {
Pattern string `mapstructure:"pattern"`
BlockFormat string `mapstructure:"block_format"`
AdFormat string `mapstructure:"ad_format"`
}
func (o *GoogleSearchAdsOptions) fromMap(m map[string]interface{}) error {
return mapstructure.Decode(m, o)
}
type PingProxiesOptions struct {
Interval time.Duration
}
func (o *PingProxiesOptions) fromMap(m map[string]interface{}) error {
interval, err := durationFromMap(m, "interval")
if err != nil {
return err
}
o.Interval = interval
return nil
}
// TrafficLogOptions represents options for github.com/getlantern/trafficlog-flashlight.
type TrafficLogOptions struct {
// Size of the traffic log's packet buffers (if enabled).
CaptureBytes int
SaveBytes int
// How far back to go when attaching packets to an issue report.
CaptureSaveDuration time.Duration
// Whether to overwrite the traffic log binary. This may result in users being re-prompted for
// their passwords. The binary will never be overwritten if the existing binary matches the
// embedded version.
Reinstall bool
// The minimum amount of time to wait before re-prompting the user since the last time we failed
// to install the traffic log. The most likely reason for a failed install is denial of
// permission by the user. A value of 0 means we never re-attempt installation.
WaitTimeSinceFailedInstall time.Duration
// The number of times installation can fail before we give up on this client. A value of zero
// is equivalent to a value of one.
FailuresThreshold int
// After this amount of time has elapsed, the failure count is reset and a user may be
// re-prompted to install the traffic log.
TimeBeforeFailureReset time.Duration
// The number of times a user must deny permission for the traffic log before we stop asking. A
// value of zero is equivalent to a value of one.
UserDenialThreshold int
// After this amount of time has elapsed, the user denial count is reset and a user may be
// re-prompted to install the traffic log.
TimeBeforeDenialReset time.Duration
}
func (o *TrafficLogOptions) fromMap(m map[string]interface{}) error {
var err error
o.CaptureBytes, err = intFromMap(m, "capturebytes")
if err != nil {
return errors.New("error unmarshaling 'capturebytes': %v", err)
}
o.SaveBytes, err = intFromMap(m, "savebytes")
if err != nil {
return errors.New("error unmarshaling 'savebytes': %v", err)
}
o.CaptureSaveDuration, err = durationFromMap(m, "capturesaveduration")
if err != nil {
return errors.New("error unmarshaling 'capturesaveduration': %v", err)
}
o.Reinstall, err = boolFromMap(m, "reinstall")
if err != nil {
return errors.New("error unmarshaling 'reinstall': %v", err)
}
o.WaitTimeSinceFailedInstall, err = durationFromMap(m, "waittimesincefailedinstall")
if err != nil {
return errors.New("error unmarshaling 'waittimesincefailedinstall': %v", err)
}
o.UserDenialThreshold, err = intFromMap(m, "userdenialthreshold")
if err != nil {
return errors.New("error unmarshaling 'userdenialthreshold': %v", err)
}
o.TimeBeforeDenialReset, err = durationFromMap(m, "timebeforedenialreset")
if err != nil {
return errors.New("error unmarshaling 'timebeforedenialreset': %v", err)
}
return nil
}
// ClientGroup represents a subgroup of Lantern clients chosen randomly or
// based on certain criteria on which features can be selectively turned on.
type ClientGroup struct {
// A label so that the group can be referred to when collecting/analyzing
// metrics. Better to be unique and meaningful.
Label string
// UserFloor and UserCeil defines the range of user IDs so that with
// precision p, any user ID u satisfies floor*p <= u%p < ceil*p belongs to
// the group. Precision is expressed in the code and can be changed freely.
//
// For example, given floor = 0.1 and ceil = 0.2, it matches user IDs end
// between 100 and 199 if precision is 1000, and IDs end between 1000 and
// 1999 if precision is 10000.
//
// Range: 0-1. When both are omitted, all users fall within the range.
UserFloor float64
UserCeil float64
// The application the feature applies to. Defaults to all applications.
Application string
// A semantic version range which only Lantern versions falls within is consided.
// Defaults to all versions.
VersionConstraints string
// Comma separated list of platforms the group includes.
// Defaults to all platforms.
Platforms string
// Only include Lantern Free clients.
FreeOnly bool
// Only include Lantern Pro clients.
ProOnly bool
// Comma separated list of countries the group includes.
// Defaults to all countries.
GeoCountries string
// Random fraction of clients to include from the final set where all other
// criteria match.
//
// Range: 0-1. Defaults to 1.
Fraction float64
}
// Validate checks if the ClientGroup fields are valid and do not conflict with
// each other.
func (g ClientGroup) Validate() error {
if g.UserFloor < 0 || g.UserFloor > 1.0 {
return errors.New("Invalid UserFloor")
}
if g.UserCeil < 0 || g.UserCeil > 1.0 {
return errors.New("Invalid UserCeil")
}
if g.UserCeil < g.UserFloor {
return errors.New("Invalid user range")
}
if g.Fraction < 0 || g.Fraction > 1.0 {
return errors.New("Invalid Fraction")
}
if g.FreeOnly && g.ProOnly {
return errors.New("Both FreeOnly and ProOnly is set")
}
if g.VersionConstraints != "" {
_, err := semver.ParseRange(g.VersionConstraints)
if err != nil {
return fmt.Errorf("error parsing version constraints: %v", err)
}
}
return nil
}
// Includes checks if the ClientGroup includes the user, device and country
// combination, assuming the group has been validated.
func (g ClientGroup) Includes(platform, appName, version string, userID int64, isPro bool, geoCountry string) bool {
if g.UserCeil > 0 {
// Unknown user ID doesn't belong to any user range
if userID == 0 {
return false
}
precision := 1000.0
remainder := userID % int64(precision)
if remainder < int64(g.UserFloor*precision) || remainder >= int64(g.UserCeil*precision) {
return false
}
}
if g.FreeOnly && isPro {
return false
}
if g.ProOnly && !isPro {
return false
}
if g.Application != "" && strings.ToLower(g.Application) != strings.ToLower(appName) {
return false
}
if g.VersionConstraints != "" {
expectedRange, err := semver.ParseRange(g.VersionConstraints)
if err != nil {
return false
}
if !expectedRange(semver.MustParse(version)) {
return false
}
}
if g.Platforms != "" && !csvContains(g.Platforms, platform) {
return false
}
if g.GeoCountries != "" && !csvContains(g.GeoCountries, geoCountry) {
return false
}
if g.Fraction > 0 && randomFloat >= g.Fraction {
return false
}
return true
}
func csvContains(csv, s string) bool {
fields := strings.Split(csv, ",")
for _, f := range fields {
if strings.EqualFold(s, strings.TrimSpace(f)) {
return true
}
}
return false
}
func boolFromMap(m map[string]interface{}, name string) (bool, error) {
v, exists := m[name]
if !exists {
return false, errAbsentOption
}
b, ok := v.(bool)
if !ok {
return false, errMalformedOption
}
return b, nil
}
func intFromMap(m map[string]interface{}, name string) (int, error) {
v, exists := m[name]
if !exists {
return 0, errAbsentOption
}
i, ok := v.(int)
if !ok {
return 0, errMalformedOption
}
return i, nil
}
func durationFromMap(m map[string]interface{}, name string) (time.Duration, error) {
v, exists := m[name]
if !exists {
return 0, errAbsentOption
}
s, ok := v.(string)
if !ok {
return 0, errMalformedOption
}
d, err := time.ParseDuration(s)
if err != nil {
return 0, errMalformedOption
}
return d, nil
}