github.com/craicoverflow/tyk@v2.9.6-rc3+incompatible/gateway/mw_url_rewrite.go (about)

     1  package gateway
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/http"
     7  	"net/textproto"
     8  	"net/url"
     9  	"reflect"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/sirupsen/logrus"
    14  
    15  	"github.com/TykTechnologies/tyk/apidef"
    16  	"github.com/TykTechnologies/tyk/ctx"
    17  	"github.com/TykTechnologies/tyk/regexp"
    18  	"github.com/TykTechnologies/tyk/user"
    19  )
    20  
    21  const (
    22  	metaLabel        = "$tyk_meta."
    23  	contextLabel     = "$tyk_context."
    24  	triggerKeyPrefix = "trigger"
    25  	triggerKeySep    = "-"
    26  )
    27  
    28  var dollarMatch = regexp.MustCompile(`\$\d+`)
    29  var contextMatch = regexp.MustCompile(`\$tyk_context.([A-Za-z0-9_\-\.]+)`)
    30  var metaMatch = regexp.MustCompile(`\$tyk_meta.([A-Za-z0-9_\-\.]+)`)
    31  
    32  func urlRewrite(meta *apidef.URLRewriteMeta, r *http.Request) (string, error) {
    33  	path := r.URL.String()
    34  	log.Debug("Inbound path: ", path)
    35  	newpath := path
    36  
    37  	if meta.MatchRegexp == nil {
    38  		var err error
    39  		meta.MatchRegexp, err = regexp.Compile(meta.MatchPattern)
    40  		if err != nil {
    41  			return path, fmt.Errorf("URLRewrite regexp error %s", meta.MatchPattern)
    42  		}
    43  	}
    44  
    45  	// Check triggers
    46  	rewriteToPath := meta.RewriteTo
    47  	if len(meta.Triggers) > 0 {
    48  
    49  		// This feature uses context, we must force it if it doesn't exist
    50  		contextData := ctxGetData(r)
    51  		if contextData == nil {
    52  			contextDataObject := make(map[string]interface{})
    53  			ctxSetData(r, contextDataObject)
    54  		}
    55  
    56  		for tn, triggerOpts := range meta.Triggers {
    57  			checkAny := false
    58  			setCount := 0
    59  			if triggerOpts.On == apidef.Any {
    60  				checkAny = true
    61  			}
    62  
    63  			// Check headers
    64  			if len(triggerOpts.Options.HeaderMatches) > 0 {
    65  				if checkHeaderTrigger(r, triggerOpts.Options.HeaderMatches, checkAny, tn) {
    66  					setCount += 1
    67  					if checkAny {
    68  						rewriteToPath = triggerOpts.RewriteTo
    69  						break
    70  					}
    71  				}
    72  			}
    73  
    74  			// Check query string
    75  			if len(triggerOpts.Options.QueryValMatches) > 0 {
    76  				if checkQueryString(r, triggerOpts.Options.QueryValMatches, checkAny, tn) {
    77  					setCount += 1
    78  					if checkAny {
    79  						rewriteToPath = triggerOpts.RewriteTo
    80  						break
    81  					}
    82  				}
    83  			}
    84  
    85  			// Check path parts
    86  			if len(triggerOpts.Options.PathPartMatches) > 0 {
    87  				if checkPathParts(r, triggerOpts.Options.PathPartMatches, checkAny, tn) {
    88  					setCount += 1
    89  					if checkAny {
    90  						rewriteToPath = triggerOpts.RewriteTo
    91  						break
    92  					}
    93  				}
    94  			}
    95  
    96  			// Check session meta
    97  			if session := ctxGetSession(r); session != nil {
    98  				if len(triggerOpts.Options.SessionMetaMatches) > 0 {
    99  					if checkSessionTrigger(r, session, triggerOpts.Options.SessionMetaMatches, checkAny, tn) {
   100  						setCount += 1
   101  						if checkAny {
   102  							rewriteToPath = triggerOpts.RewriteTo
   103  							break
   104  						}
   105  					}
   106  				}
   107  			}
   108  
   109  			// Request context meta
   110  			if len(triggerOpts.Options.RequestContextMatches) > 0 {
   111  				if checkContextTrigger(r, triggerOpts.Options.RequestContextMatches, checkAny, tn) {
   112  					setCount += 1
   113  					if checkAny {
   114  						rewriteToPath = triggerOpts.RewriteTo
   115  						break
   116  					}
   117  				}
   118  			}
   119  
   120  			// Check payload
   121  			if triggerOpts.Options.PayloadMatches.MatchPattern != "" {
   122  				if checkPayload(r, triggerOpts.Options.PayloadMatches, tn) {
   123  					setCount += 1
   124  					if checkAny {
   125  						rewriteToPath = triggerOpts.RewriteTo
   126  						break
   127  					}
   128  				}
   129  			}
   130  
   131  			if !checkAny {
   132  				// Set total count:
   133  				total := 0
   134  				if len(triggerOpts.Options.HeaderMatches) > 0 {
   135  					total += 1
   136  				}
   137  				if len(triggerOpts.Options.QueryValMatches) > 0 {
   138  					total += 1
   139  				}
   140  				if len(triggerOpts.Options.PathPartMatches) > 0 {
   141  					total += 1
   142  				}
   143  				if len(triggerOpts.Options.SessionMetaMatches) > 0 {
   144  					total += 1
   145  				}
   146  				if len(triggerOpts.Options.RequestContextMatches) > 0 {
   147  					total += 1
   148  				}
   149  				if triggerOpts.Options.PayloadMatches.MatchPattern != "" {
   150  					total += 1
   151  				}
   152  				if total == setCount {
   153  					rewriteToPath = triggerOpts.RewriteTo
   154  				}
   155  			}
   156  		}
   157  	}
   158  
   159  	matchGroups := meta.MatchRegexp.FindAllStringSubmatch(path, -1)
   160  
   161  	// Make sure it matches the string
   162  	log.Debug("Rewriter checking matches, len is: ", len(matchGroups))
   163  	if len(matchGroups) > 0 {
   164  		newpath = rewriteToPath
   165  		// get the indices for the replacements:
   166  		replaceGroups := dollarMatch.FindAllStringSubmatch(rewriteToPath, -1)
   167  
   168  		log.Debug(matchGroups)
   169  		log.Debug(replaceGroups)
   170  
   171  		groupReplace := make(map[string]string)
   172  		for mI, replacementVal := range matchGroups[0] {
   173  			indexVal := "$" + strconv.Itoa(mI)
   174  			groupReplace[indexVal] = replacementVal
   175  		}
   176  
   177  		for _, v := range replaceGroups {
   178  			newpath = strings.Replace(newpath, v[0], groupReplace[v[0]], -1)
   179  		}
   180  
   181  		log.Debug("URL Re-written from: ", path)
   182  		log.Debug("URL Re-written to: ", newpath)
   183  
   184  		// put url_rewrite path to context to be used in ResponseTransformMiddleware
   185  		ctxSetUrlRewritePath(r, meta.Path)
   186  	}
   187  
   188  	newpath = replaceTykVariables(r, newpath, true)
   189  
   190  	return newpath, nil
   191  }
   192  
   193  func replaceTykVariables(r *http.Request, in string, escape bool) string {
   194  	if strings.Contains(in, contextLabel) {
   195  		contextData := ctxGetData(r)
   196  		vars := contextMatch.FindAllString(in, -1)
   197  		in = replaceVariables(in, vars, contextData, contextLabel, escape)
   198  	}
   199  
   200  	if strings.Contains(in, metaLabel) {
   201  		vars := metaMatch.FindAllString(in, -1)
   202  		session := ctxGetSession(r)
   203  		if session == nil {
   204  			in = replaceVariables(in, vars, nil, metaLabel, escape)
   205  		} else {
   206  			in = replaceVariables(in, vars, session.GetMetaData(), metaLabel, escape)
   207  		}
   208  	}
   209  	//todo add config_data
   210  	return in
   211  }
   212  
   213  func replaceVariables(in string, vars []string, vals map[string]interface{}, label string, escape bool) string {
   214  	for _, v := range vars {
   215  		key := strings.Replace(v, label, "", 1)
   216  		val, ok := vals[key]
   217  		if ok {
   218  			valStr := valToStr(val)
   219  			// If contains url with domain
   220  			if escape && !strings.HasPrefix(valStr, "http") {
   221  				valStr = url.QueryEscape(valStr)
   222  			}
   223  			in = strings.Replace(in, v, valStr, -1)
   224  		} else {
   225  			in = strings.Replace(in, v, "", -1)
   226  			log.WithFields(logrus.Fields{
   227  				"key":       key,
   228  				"value":     v,
   229  				"in string": in,
   230  			}).Debug("Replaced with an empty string")
   231  		}
   232  	}
   233  	return in
   234  }
   235  
   236  func valToStr(v interface{}) string {
   237  	s := ""
   238  	switch x := v.(type) {
   239  	case string:
   240  		s = x
   241  	case float64:
   242  		s = strconv.FormatFloat(x, 'f', -1, 32)
   243  	case int64:
   244  		s = strconv.FormatInt(x, 10)
   245  	case []string:
   246  		s = strings.Join(x, ",")
   247  		// Remove empty start
   248  		s = strings.TrimPrefix(s, ",")
   249  	case url.Values:
   250  		i := 0
   251  		for key, v := range x {
   252  			s += key + ":" + strings.Join(v, ",")
   253  			if i < len(x)-1 {
   254  				s += ";"
   255  			}
   256  			i++
   257  		}
   258  	case []interface{}:
   259  		tmpSlice := make([]string, 0, len(x))
   260  		for _, val := range x {
   261  			if rec := valToStr(val); rec != "" {
   262  				tmpSlice = append(tmpSlice, url.QueryEscape(rec))
   263  			}
   264  		}
   265  		s = strings.Join(tmpSlice, ",")
   266  	default:
   267  		log.Error("Context variable type is not supported: ", reflect.TypeOf(v))
   268  	}
   269  	return s
   270  }
   271  
   272  // URLRewriteMiddleware Will rewrite an inbund URL to a matching outbound one, it can also handle dynamic variable substitution
   273  type URLRewriteMiddleware struct {
   274  	BaseMiddleware
   275  }
   276  
   277  func (m *URLRewriteMiddleware) Name() string {
   278  	return "URLRewriteMiddleware"
   279  }
   280  
   281  func (m *URLRewriteMiddleware) InitTriggerRx() {
   282  	// Generate regexp for each special match parameter
   283  	for verKey := range m.Spec.VersionData.Versions {
   284  		for pathKey := range m.Spec.VersionData.Versions[verKey].ExtendedPaths.URLRewrite {
   285  			rewrite := m.Spec.VersionData.Versions[verKey].ExtendedPaths.URLRewrite[pathKey]
   286  
   287  			for trKey := range rewrite.Triggers {
   288  				tr := rewrite.Triggers[trKey]
   289  
   290  				for key, h := range tr.Options.HeaderMatches {
   291  					h.Init()
   292  					tr.Options.HeaderMatches[key] = h
   293  				}
   294  				for key, q := range tr.Options.QueryValMatches {
   295  					q.Init()
   296  					tr.Options.QueryValMatches[key] = q
   297  				}
   298  				for key, h := range tr.Options.SessionMetaMatches {
   299  					h.Init()
   300  					tr.Options.SessionMetaMatches[key] = h
   301  				}
   302  				for key, h := range tr.Options.RequestContextMatches {
   303  					h.Init()
   304  					tr.Options.RequestContextMatches[key] = h
   305  				}
   306  				for key, h := range tr.Options.PathPartMatches {
   307  					h.Init()
   308  					tr.Options.PathPartMatches[key] = h
   309  				}
   310  				if tr.Options.PayloadMatches.MatchPattern != "" {
   311  					tr.Options.PayloadMatches.Init()
   312  				}
   313  
   314  				rewrite.Triggers[trKey] = tr
   315  			}
   316  
   317  			m.Spec.VersionData.Versions[verKey].ExtendedPaths.URLRewrite[pathKey] = rewrite
   318  		}
   319  	}
   320  }
   321  
   322  func (m *URLRewriteMiddleware) EnabledForSpec() bool {
   323  	for _, version := range m.Spec.VersionData.Versions {
   324  		if len(version.ExtendedPaths.URLRewrite) > 0 {
   325  			m.Spec.URLRewriteEnabled = true
   326  			m.InitTriggerRx()
   327  			return true
   328  		}
   329  	}
   330  	return false
   331  }
   332  
   333  func (m *URLRewriteMiddleware) CheckHostRewrite(oldPath, newTarget string, r *http.Request) {
   334  	oldAsURL, _ := url.Parse(oldPath)
   335  	newAsURL, _ := url.Parse(newTarget)
   336  	if newAsURL.Scheme != LoopScheme && oldAsURL.Host != newAsURL.Host {
   337  		log.Debug("Detected a host rewrite in pattern!")
   338  		setCtxValue(r, ctx.RetainHost, true)
   339  	}
   340  }
   341  
   342  const LoopScheme = "tyk"
   343  
   344  var NonAlphaNumRE = regexp.MustCompile("[^A-Za-z0-9]+")
   345  var LoopHostRE = regexp.MustCompile("tyk://([^/]+)")
   346  
   347  func replaceNonAlphaNumeric(in string) string {
   348  	return NonAlphaNumRE.ReplaceAllString(in, "-")
   349  }
   350  
   351  // ProcessRequest will run any checks on the request on the way through the system, return an error to have the chain fail
   352  func (m *URLRewriteMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) {
   353  	_, versionPaths, _, _ := m.Spec.Version(r)
   354  	found, meta := m.Spec.CheckSpecMatchesStatus(r, versionPaths, URLRewrite)
   355  
   356  	if !found {
   357  		return nil, http.StatusOK
   358  	}
   359  
   360  	//Used for looping feature
   361  	//To get host and query parameters
   362  	ctxSetOrigRequestURL(r, r.URL)
   363  
   364  	log.Debug("Rewriter active")
   365  	umeta := meta.(*apidef.URLRewriteMeta)
   366  	log.Debug(r.URL)
   367  	oldPath := r.URL.String()
   368  	p, err := urlRewrite(umeta, r)
   369  	if err != nil {
   370  		log.Error(err)
   371  		return err, http.StatusInternalServerError
   372  	}
   373  
   374  	// During looping target can be API name
   375  	// Need make it compatible with URL parser
   376  	if strings.HasPrefix(p, LoopScheme) {
   377  		p = LoopHostRE.ReplaceAllStringFunc(p, func(match string) string {
   378  			host := strings.TrimPrefix(match, LoopScheme+"://")
   379  			return LoopScheme + "://" + replaceNonAlphaNumeric(host)
   380  		})
   381  	}
   382  
   383  	m.CheckHostRewrite(oldPath, p, r)
   384  
   385  	newURL, err := url.Parse(p)
   386  	if err != nil {
   387  		log.Error("URL Rewrite failed, could not parse: ", p)
   388  	} else {
   389  		//Setting new path here breaks request middleware
   390  		//New path is set in DummyProxyHandler/Cache middleware
   391  		ctxSetURLRewriteTarget(r, newURL)
   392  	}
   393  	return nil, http.StatusOK
   394  }
   395  
   396  func checkHeaderTrigger(r *http.Request, options map[string]apidef.StringRegexMap, any bool, triggernum int) bool {
   397  	contextData := ctxGetData(r)
   398  	fCount := 0
   399  	for mh, mr := range options {
   400  		mhCN := textproto.CanonicalMIMEHeaderKey(mh)
   401  		vals, ok := r.Header[mhCN]
   402  		if ok {
   403  			for i, v := range vals {
   404  				matched, match := mr.FindStringSubmatch(v)
   405  				if matched {
   406  					addMatchToContextData(contextData, match, triggernum, mhCN, i)
   407  					fCount++
   408  				}
   409  			}
   410  		}
   411  	}
   412  
   413  	if fCount > 0 {
   414  		ctxSetData(r, contextData)
   415  		if any {
   416  			return true
   417  		}
   418  
   419  		return len(options) <= fCount
   420  	}
   421  
   422  	return false
   423  }
   424  
   425  func checkQueryString(r *http.Request, options map[string]apidef.StringRegexMap, any bool, triggernum int) bool {
   426  	contextData := ctxGetData(r)
   427  	fCount := 0
   428  	for mv, mr := range options {
   429  		qvals := r.URL.Query()
   430  		vals, ok := qvals[mv]
   431  		if ok {
   432  			for i, v := range vals {
   433  				matched, match := mr.FindStringSubmatch(v)
   434  				if matched {
   435  					addMatchToContextData(contextData, match, triggernum, mv, i)
   436  					fCount++
   437  				}
   438  			}
   439  		}
   440  	}
   441  
   442  	if fCount > 0 {
   443  		ctxSetData(r, contextData)
   444  		if any {
   445  			return true
   446  		}
   447  
   448  		return len(options) <= fCount
   449  	}
   450  
   451  	return false
   452  }
   453  
   454  func checkPathParts(r *http.Request, options map[string]apidef.StringRegexMap, any bool, triggernum int) bool {
   455  	contextData := ctxGetData(r)
   456  	fCount := 0
   457  	for mv, mr := range options {
   458  		pathParts := strings.Split(r.URL.Path, "/")
   459  
   460  		for _, part := range pathParts {
   461  			matched, match := mr.FindStringSubmatch(part)
   462  			if matched {
   463  				addMatchToContextData(contextData, match, triggernum, mv, fCount)
   464  				fCount++
   465  			}
   466  		}
   467  	}
   468  
   469  	if fCount > 0 {
   470  		ctxSetData(r, contextData)
   471  		if any {
   472  			return true
   473  		}
   474  
   475  		return len(options) <= fCount
   476  	}
   477  
   478  	return false
   479  }
   480  
   481  func checkSessionTrigger(r *http.Request, sess *user.SessionState, options map[string]apidef.StringRegexMap, any bool, triggernum int) bool {
   482  	contextData := ctxGetData(r)
   483  	fCount := 0
   484  	for mh, mr := range options {
   485  		rawVal, ok := sess.GetMetaDataByKey(mh)
   486  		if ok {
   487  			val, valOk := rawVal.(string)
   488  			if valOk {
   489  				matched, match := mr.FindStringSubmatch(val)
   490  				if matched {
   491  					addMatchToContextData(contextData, match, triggernum, mh)
   492  					fCount++
   493  				}
   494  			}
   495  		}
   496  	}
   497  
   498  	if fCount > 0 {
   499  		ctxSetData(r, contextData)
   500  		if any {
   501  			return true
   502  		}
   503  
   504  		return len(options) <= fCount
   505  	}
   506  
   507  	return false
   508  }
   509  
   510  func checkContextTrigger(r *http.Request, options map[string]apidef.StringRegexMap, any bool, triggernum int) bool {
   511  	contextData := ctxGetData(r)
   512  	fCount := 0
   513  
   514  	for mh, mr := range options {
   515  		rawVal, ok := contextData[mh]
   516  
   517  		if ok {
   518  			val, valOk := rawVal.(string)
   519  			if valOk {
   520  				matched, match := mr.FindStringSubmatch(val)
   521  				if matched {
   522  					addMatchToContextData(contextData, match, triggernum, mh)
   523  					fCount++
   524  				}
   525  			}
   526  		}
   527  	}
   528  
   529  	if fCount > 0 {
   530  		ctxSetData(r, contextData)
   531  		if any {
   532  			return true
   533  		}
   534  
   535  		return len(options) <= fCount
   536  	}
   537  
   538  	return false
   539  }
   540  
   541  func checkPayload(r *http.Request, options apidef.StringRegexMap, triggernum int) bool {
   542  	contextData := ctxGetData(r)
   543  	bodyBytes, _ := ioutil.ReadAll(r.Body)
   544  
   545  	matched, matches := options.FindAllStringSubmatch(string(bodyBytes), -1)
   546  
   547  	if matched {
   548  		kn := buildTriggerKey(triggernum, "payload")
   549  		if len(matches) == 0 {
   550  			return true
   551  		}
   552  		contextData[kn] = matches[0][0]
   553  
   554  		for i, match := range matches {
   555  			if len(match) > 0 {
   556  				addMatchToContextData(contextData, match, triggernum, "payload", i)
   557  			}
   558  		}
   559  		return true
   560  	}
   561  
   562  	return false
   563  }
   564  
   565  func addMatchToContextData(cd map[string]interface{}, match []string, trNum int, trName string, indices ...int) {
   566  	kn := buildTriggerKey(trNum, trName, indices...)
   567  	if len(match) == 0 {
   568  		return
   569  	}
   570  
   571  	cd[kn] = match[0]
   572  
   573  	if len(match) > 1 {
   574  		addGroupsToContextData(cd, kn, match[1:])
   575  	}
   576  }
   577  
   578  func buildTriggerKey(num int, name string, indices ...int) string {
   579  	parts := []string{triggerKeyPrefix, strconv.Itoa(num), name}
   580  
   581  	if len(indices) > 0 {
   582  		for _, index := range indices {
   583  			parts = append(parts, strconv.Itoa(index))
   584  		}
   585  	}
   586  
   587  	return strings.Join(parts, triggerKeySep)
   588  }
   589  
   590  func addGroupsToContextData(cd map[string]interface{}, keyPrefix string, groups []string) {
   591  	for i, g := range groups {
   592  		k := strings.Join([]string{keyPrefix, strconv.Itoa(i)}, triggerKeySep)
   593  		cd[k] = g
   594  	}
   595  }