-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathfeatures.go
302 lines (274 loc) · 9.15 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
package config
import (
"fmt"
"math/rand"
"reflect"
"regexp"
"strings"
"time"
"github.com/blang/semver"
"github.com/mitchellh/mapstructure"
"github.com/getlantern/errors"
globalConfig "github.com/getlantern/flashlight/v7/config/global"
)
const (
FeatureAuth = "auth"
FeatureProxyBench = "proxybench"
FeatureTrafficLog = "trafficlog"
FeatureNoBorda = "noborda"
FeatureProbeProxies = "probeproxies"
FeatureShortcut = "shortcut"
FeatureDetour = "detour"
FeatureNoHTTPSEverywhere = "nohttpseverywhere"
FeatureReplica = globalConfig.FeatureReplica
FeatureProxyWhitelistedOnly = "proxywhitelistedonly"
FeatureTrackYouTube = "trackyoutube"
FeatureGoogleSearchAds = "googlesearchads"
FeatureChat = "chat"
FeatureOtel = "otel"
FeatureInterstitialAds = "interstitialads"
FeatureTapsellAds = "tapsellads"
)
var (
// to have stable calculation of fraction until the client restarts.
randomFloat = rand.Float64()
errAbsentOption = globalConfig.ErrFeatureOptionAbsent
errMalformedOption = errors.New("malformed option")
)
type FeatureOptions = globalConfig.FeatureOptions
type GoogleSearchAdsOptions struct {
Pattern string `mapstructure:"pattern"`
BlockFormat string `mapstructure:"block_format"`
AdFormat string `mapstructure:"ad_format"`
Partners map[string][]PartnerAd `mapstructure:"partners"`
}
type PartnerAd struct {
Name string
URL string
Campaign string
Description string
Keywords []*regexp.Regexp
Probability float32
}
func (o *GoogleSearchAdsOptions) FromMap(m map[string]interface{}) error {
// since keywords can be regexp and we don't want to compile them each time we compare, define a custom decode hook
// that will convert string to regexp and error out on syntax issues
config := &mapstructure.DecoderConfig{
DecodeHook: func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if t != reflect.TypeOf(regexp.Regexp{}) {
return data, nil
}
r, err := regexp.Compile(fmt.Sprintf("%v", data))
if err != nil {
return nil, err
}
return r, nil
},
Result: o,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return err
}
return decoder.Decode(m)
}
// 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 = somethingFromMap[int](m, "capturebytes")
if err != nil {
return errors.New("error unmarshaling 'capturebytes': %v", err)
}
o.SaveBytes, err = somethingFromMap[int](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 = somethingFromMap[bool](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 = somethingFromMap[int](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.EqualFold(g.Application, 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 somethingFromMap[T any](m map[string]interface{}, name string) (T, error) {
var ret T
v, exists := m[name]
if !exists {
return ret, errAbsentOption
}
var ok bool
ret, ok = v.(T)
if !ok {
return ret, errMalformedOption
}
return ret, nil
}
func durationFromMap(m map[string]interface{}, name string) (time.Duration, error) {
s, err := somethingFromMap[string](m, name)
if err != nil {
return 0, err
}
d, err := time.ParseDuration(s)
if err != nil {
return 0, errMalformedOption
}
return d, nil
}