...

Source file src/github.com/growthbook/growthbook-golang/util.go

Documentation: github.com/growthbook/growthbook-golang

     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  // Returns an array of floats with numVariations items that are all
    16  // equal and sum to 1.
    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  // Checks if an experiment variation is being forced via a URL query
    29  // string.
    30  //
    31  // As an example, if the id is "my-test" and url is
    32  // http://localhost/?my-test=1, this function returns 1.
    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  // Remove PKCS #7 padding.
    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  // This function imitates Javascript's "truthiness" evaluation for Go
   120  // values of unknown type.
   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  // This function converts slices of concrete types to []interface{}.
   143  // This is needed to handle the common case where a user passes an
   144  // attribute as a []string (or []int), and this needs to be compared
   145  // against feature data deserialized from JSON, which always results
   146  // in []interface{} slices.
   147  func fixSliceTypes(vin interface{}) interface{} {
   148  	// Convert all type-specific slices to interface{} slices.
   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  	// If a protocol is missing, but a host is specified, add `https://`
   208  	// to the front. Use "_____" as the wildcard since `*` is not a valid
   209  	// hostname in some browsers
   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  	// Compare each part of the URL separately
   224  	comps := []comp{
   225  		{actual.Host, expected.Host, false},
   226  		{actual.Path, expected.Path, true},
   227  	}
   228  	// We only want to compare hashes if it's explicitly being targeted
   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  	// If any comparisons fail, the whole thing fails
   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  	// Escape special regex characters.
   262  	specialRe := regexp.MustCompile(`([*.+?^${}()|[\]\\])`)
   263  	escaped := specialRe.ReplaceAllString(pattern, "\\$1")
   264  	escaped = strings.Replace(escaped, "_____", ".*", -1)
   265  
   266  	if isPath {
   267  		// When matching pathname, make leading/trailing slashes optional
   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  	// Remove build info and leading `v` if any
   362  	// Split version into parts (both core version numbers and pre-release tags)
   363  	// "v1.2.3-rc.1+build123" -> ["1","2","3","rc","1"]
   364  	stripped := versionStripRe.ReplaceAllLiteralString(input, "")
   365  	parts := versionSplitRe.Split(stripped, -1)
   366  
   367  	// If it's SemVer without a pre-release, add `~` to the end
   368  	// ["1","0","0"] -> ["1","0","0","~"]
   369  	// "~" is the largest ASCII character, so this will make "1.0.0"
   370  	// greater than "1.0.0-beta" for example
   371  	if len(parts) == 3 {
   372  		parts = append(parts, "~")
   373  	}
   374  
   375  	// Left pad each numeric part with spaces so string comparisons will
   376  	// work ("9">"10", but " 9"<"10")
   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  	// Then, join back together into a single string
   383  	return strings.Join(parts, "-")
   384  }
   385  

View as plain text