1 package growthbook
2
3 import (
4 "crypto/aes"
5 "crypto/cipher"
6 "encoding/base64"
7 "errors"
8 "net/url"
9 "reflect"
10 "regexp"
11 "strconv"
12 "strings"
13 )
14
15
16
17 func getEqualWeights(numVariations int) []float64 {
18 if numVariations < 0 {
19 numVariations = 0
20 }
21 equal := make([]float64, numVariations)
22 for i := range equal {
23 equal[i] = 1.0 / float64(numVariations)
24 }
25 return equal
26 }
27
28
29
30
31
32
33 func getQueryStringOverride(id string, url *url.URL, numVariations int) *int {
34 v, ok := url.Query()[id]
35 if !ok || len(v) > 1 {
36 return nil
37 }
38
39 vi, err := strconv.Atoi(v[0])
40 if err != nil {
41 return nil
42 }
43
44 if vi < 0 || vi >= numVariations {
45 return nil
46 }
47
48 return &vi
49 }
50
51 func decrypt(encrypted string, encKey string) (string, error) {
52 key, err := base64.StdEncoding.DecodeString(encKey)
53 if err != nil {
54 return "", err
55 }
56
57 splits := strings.Split(encrypted, ".")
58 if len(splits) != 2 {
59 return "", errors.New("invalid format for key")
60 }
61
62 iv, err := base64.StdEncoding.DecodeString(splits[0])
63 if err != nil {
64 return "", err
65 }
66
67 cipherText, err := base64.StdEncoding.DecodeString(splits[1])
68 if err != nil {
69 return "", err
70 }
71
72 block, err := aes.NewCipher(key)
73 if err != nil {
74 return "", err
75 }
76
77 if len(iv) != block.BlockSize() {
78 return "", errors.New("invalid IV length")
79 }
80
81 mode := cipher.NewCBCDecrypter(block, iv)
82 mode.CryptBlocks(cipherText, cipherText)
83
84 cipherText, err = unpad(cipherText)
85 if err != nil {
86 return "", err
87 }
88
89 return string(cipherText), nil
90 }
91
92
93
94 func unpad(buf []byte) ([]byte, error) {
95 bufLen := len(buf)
96 if bufLen == 0 {
97 return nil, errors.New("crypto/padding: invalid padding size")
98 }
99
100 pad := buf[bufLen-1]
101 if pad == 0 {
102 return nil, errors.New("crypto/padding: invalid last byte of padding")
103 }
104
105 padLen := int(pad)
106 if padLen > bufLen || padLen > 16 {
107 return nil, errors.New("crypto/padding: invalid padding size")
108 }
109
110 for _, v := range buf[bufLen-padLen : bufLen-1] {
111 if v != pad {
112 return nil, errors.New("crypto/padding: invalid padding")
113 }
114 }
115
116 return buf[:bufLen-padLen], nil
117 }
118
119
120
121 func truthy(v interface{}) bool {
122 if v == nil {
123 return false
124 }
125 switch v.(type) {
126 case string:
127 return v.(string) != ""
128 case bool:
129 return v.(bool)
130 case int:
131 return v.(int) != 0
132 case uint:
133 return v.(uint) != 0
134 case float32:
135 return v.(float32) != 0
136 case float64:
137 return v.(float64) != 0
138 }
139 return true
140 }
141
142
143
144
145
146
147 func fixSliceTypes(vin interface{}) interface{} {
148
149 v := reflect.ValueOf(vin)
150 rv := vin
151 if v.Kind() == reflect.Slice || v.Kind() == reflect.Array {
152 srv := make([]interface{}, v.Len())
153 for i := 0; i < v.Len(); i++ {
154 elem := v.Index(i).Interface()
155 srv[i] = elem
156 }
157 rv = srv
158 }
159 return rv
160 }
161
162 func isURLTargeted(url *url.URL, targets []URLTarget) bool {
163 if len(targets) == 0 {
164 return false
165 }
166
167 hasIncludeRules := false
168 isIncluded := false
169
170 for _, t := range targets {
171 match := evalURLTarget(url, t.Type, t.Pattern)
172 if !t.Include {
173 if match {
174 return false
175 }
176 } else {
177 hasIncludeRules = true
178 if match {
179 isIncluded = true
180 }
181 }
182 }
183
184 return isIncluded || !hasIncludeRules
185 }
186
187 func evalURLTarget(url *url.URL, typ URLTargetType, pattern string) bool {
188 if typ == RegexURLTarget {
189 regex := getURLRegexp(pattern)
190 if regex == nil {
191 return false
192 }
193 return regex.MatchString(url.String()) || regex.MatchString(url.Path)
194 } else if typ == SimpleURLTarget {
195 return evalSimpleURLTarget(url, pattern)
196 }
197 return false
198 }
199
200 type comp struct {
201 actual string
202 expected string
203 isPath bool
204 }
205
206 func evalSimpleURLTarget(actual *url.URL, pattern string) bool {
207
208
209
210 schemeRe := regexp.MustCompile(`(?i)^([^:/?]*)\.`)
211 pattern = schemeRe.ReplaceAllString(pattern, "https://$1.")
212 wildcardRe := regexp.MustCompile(`\*`)
213 pattern = wildcardRe.ReplaceAllLiteralString(pattern, "_____")
214 expected, err := url.Parse(pattern)
215 if err != nil {
216 logError("Failed to parse URL pattern: ", pattern)
217 return false
218 }
219 if expected.Host == "" {
220 expected.Host = "_____"
221 }
222
223
224 comps := []comp{
225 {actual.Host, expected.Host, false},
226 {actual.Path, expected.Path, true},
227 }
228
229 if expected.Fragment != "" {
230 comps = append(comps, comp{actual.Fragment, expected.Fragment, false})
231 }
232
233 actualParams, err := url.ParseQuery(actual.RawQuery)
234 if err != nil {
235 logError("Failed to parse actual URL query parameters: ", actual.RawQuery)
236 return false
237 }
238 expectedParams, err := url.ParseQuery(expected.RawQuery)
239 if err != nil {
240 logError("Failed to parse expected URL query parameters: ", expected.RawQuery)
241 return false
242 }
243 for param, expectedValue := range expectedParams {
244 actualValue := ""
245 if actualParams.Has(param) {
246 actualValue = actualParams[param][0]
247 }
248 comps = append(comps, comp{actualValue, expectedValue[0], false})
249 }
250
251
252 for _, comp := range comps {
253 if !evalSimpleURLPart(comp.actual, comp.expected, comp.isPath) {
254 return false
255 }
256 }
257 return true
258 }
259
260 func evalSimpleURLPart(actual string, pattern string, isPath bool) bool {
261
262 specialRe := regexp.MustCompile(`([*.+?^${}()|[\]\\])`)
263 escaped := specialRe.ReplaceAllString(pattern, "\\$1")
264 escaped = strings.Replace(escaped, "_____", ".*", -1)
265
266 if isPath {
267
268 slashRe := regexp.MustCompile(`(^\/|\/$)`)
269 escaped = slashRe.ReplaceAllLiteralString(escaped, "")
270 escaped = "\\/?" + escaped + "\\/?"
271 }
272
273 escaped = "(?i)^" + escaped + "$"
274 regex, err := regexp.Compile(escaped)
275 if err != nil {
276 logError("Failed to compile regexp: ", escaped)
277 return false
278 }
279 return regex.MatchString(actual)
280 }
281
282 func getURLRegexp(regexString string) *regexp.Regexp {
283 retval, err := regexp.Compile(regexString)
284 if err == nil {
285 return retval
286 }
287 logError("Failed to compile URL regexp:", err)
288 return nil
289 }
290
291 func jsonString(v interface{}, typeName string, fieldName string) (string, bool) {
292 tmp, ok := v.(string)
293 if ok {
294 return tmp, true
295 }
296 logError("Invalid JSON data type", typeName, fieldName)
297 return "", false
298 }
299
300 func jsonBool(v interface{}, typeName string, fieldName string) (bool, bool) {
301 tmp, ok := v.(bool)
302 if ok {
303 return tmp, true
304 }
305 logError("Invalid JSON data type", typeName, fieldName)
306 return false, false
307 }
308
309 func jsonInt(v interface{}, typeName string, fieldName string) (int, bool) {
310 tmp, ok := v.(float64)
311 if ok {
312 return int(tmp), true
313 }
314 logError("Invalid JSON data type", typeName, fieldName)
315 return 0, false
316 }
317
318 func jsonFloat(v interface{}, typeName string, fieldName string) (float64, bool) {
319 tmp, ok := v.(float64)
320 if ok {
321 return tmp, true
322 }
323 logError("Invalid JSON data type", typeName, fieldName)
324 return 0.0, false
325 }
326
327 func jsonMaybeFloat(v interface{}, typeName string, fieldName string) (*float64, bool) {
328 tmp, ok := v.(float64)
329 if ok {
330 return &tmp, true
331 }
332 logError("Invalid JSON data type", typeName, fieldName)
333 return nil, false
334 }
335
336 func jsonFloatArray(v interface{}, typeName string, fieldName string) ([]float64, bool) {
337 vals, ok := v.([]interface{})
338 if !ok {
339 logError("Invalid JSON data type", typeName, fieldName)
340 return nil, false
341 }
342 fvals := make([]float64, len(vals))
343 for i := range vals {
344 tmp, ok := vals[i].(float64)
345 if !ok {
346 logError("Invalid JSON data type", typeName, fieldName)
347 return nil, false
348 }
349 fvals[i] = tmp
350 }
351 return fvals, true
352 }
353
354 var (
355 versionStripRe = regexp.MustCompile(`(^v|\+.*$)`)
356 versionSplitRe = regexp.MustCompile(`[-.]`)
357 versionNumRe = regexp.MustCompile(`^[0-9]+$`)
358 )
359
360 func paddedVersionString(input string) string {
361
362
363
364 stripped := versionStripRe.ReplaceAllLiteralString(input, "")
365 parts := versionSplitRe.Split(stripped, -1)
366
367
368
369
370
371 if len(parts) == 3 {
372 parts = append(parts, "~")
373 }
374
375
376
377 for i := range parts {
378 if versionNumRe.MatchString(parts[i]) {
379 parts[i] = strings.Repeat(" ", 5-len(parts[i])) + parts[i]
380 }
381 }
382
383 return strings.Join(parts, "-")
384 }
385
View as plain text