github.com/kumasuke120/mockuma@v1.1.9/internal/server/matcher.go (about)

     1  package server
     2  
     3  import (
     4  	"bytes"
     5  	"io/ioutil"
     6  	"log"
     7  	"net/http"
     8  	"regexp"
     9  
    10  	"github.com/kumasuke120/mockuma/internal/mckmaps"
    11  	"github.com/kumasuke120/mockuma/internal/myhttp"
    12  	"github.com/kumasuke120/mockuma/internal/myjson"
    13  )
    14  
    15  type pathMatcher struct {
    16  	mappings    *mckmaps.MockuMappings
    17  	directPath  map[string][]*mckmaps.Mapping
    18  	patternPath map[*regexp.Regexp][]*mckmaps.Mapping
    19  }
    20  
    21  var pathVarRegexp = regexp.MustCompile(`{(\d+)}`)
    22  
    23  func newPathMatcher(mappings *mckmaps.MockuMappings) *pathMatcher {
    24  	directPath := make(map[string][]*mckmaps.Mapping)
    25  	patternPath := make(map[*regexp.Regexp][]*mckmaps.Mapping)
    26  	for _, m := range mappings.Mappings {
    27  		if theURI := pathVarRegexp.ReplaceAllString(m.URI, "(?P<v$1>.*?)"); theURI == m.URI {
    28  			mappingsOfURI := directPath[theURI]
    29  			mappingsOfURI = append(mappingsOfURI, m)
    30  			directPath[theURI] = mappingsOfURI
    31  		} else {
    32  			regexpURI := regexp.MustCompile("^" + theURI + "$")
    33  			mappingsOfURI := patternPath[regexpURI]
    34  			mappingsOfURI = append(mappingsOfURI, m)
    35  			patternPath[regexpURI] = mappingsOfURI
    36  		}
    37  	}
    38  
    39  	return &pathMatcher{
    40  		mappings:    mappings,
    41  		directPath:  directPath,
    42  		patternPath: patternPath,
    43  	}
    44  }
    45  
    46  func (m *pathMatcher) bind(r *http.Request) *boundMatcher {
    47  	return &boundMatcher{m: m, r: r, conf: m.mappings.Config}
    48  }
    49  
    50  type boundMatcher struct {
    51  	m          *pathMatcher
    52  	conf       *mckmaps.Config
    53  	r          *http.Request
    54  	method     myhttp.HTTPMethod
    55  	uri        string
    56  	uriPattern *regexp.Regexp
    57  
    58  	matchedMapping *mckmaps.Mapping
    59  	matchState     matchState
    60  	bodyCache      []byte
    61  }
    62  
    63  type matchState int
    64  
    65  const (
    66  	matchExact = iota
    67  	matchURI
    68  	matchHead
    69  	matchCORSOptions
    70  	matchNone
    71  )
    72  
    73  func (bm *boundMatcher) matches() bool {
    74  	bm.uri = bm.r.URL.Path
    75  	bm.method = myhttp.ToHTTPMethod(bm.r.Method)
    76  
    77  	var possibleMappings []*mckmaps.Mapping
    78  	possibleMappings = bm.matchURIDirect()
    79  
    80  	var possibleURIPattern *regexp.Regexp
    81  	if len(possibleMappings) == 0 {
    82  		possibleMappings, possibleURIPattern = bm.matchURIPattern()
    83  	}
    84  
    85  	if len(possibleMappings) != 0 { // if finds any mapping
    86  		if matched := bm.matchByMethod(possibleMappings); matched != nil {
    87  			bm.matchedMapping = matched
    88  			bm.uriPattern = possibleURIPattern
    89  			bm.matchState = matchExact
    90  		} else if matched := bm.matchHead(possibleMappings); matched != nil {
    91  			bm.matchedMapping = matched
    92  			bm.matchState = matchHead
    93  		} else if bm.matchCORSOptions() {
    94  			bm.matchedMapping = nil
    95  			bm.matchState = matchCORSOptions
    96  		} else {
    97  			bm.matchedMapping = nil
    98  			bm.matchState = matchURI
    99  		}
   100  	} else {
   101  		bm.matchedMapping = nil
   102  		bm.matchState = matchNone
   103  	}
   104  
   105  	return bm.matchState != matchNone
   106  }
   107  
   108  func (bm *boundMatcher) matchURIDirect() (pm []*mckmaps.Mapping) {
   109  	if mappingsOfURI, ok := bm.m.directPath[bm.uri]; ok { // matching for direct path
   110  		pm = mappingsOfURI
   111  	} else if bm.conf.MatchTrailingSlash { // matches /path to /path/
   112  		if mappingsOfURI, ok := bm.m.directPath[bm.uri+"/"]; ok {
   113  			bm.uri += "/"
   114  			pm = mappingsOfURI
   115  		}
   116  	}
   117  	return
   118  }
   119  
   120  func (bm *boundMatcher) matchURIPattern() (pm []*mckmaps.Mapping, pp *regexp.Regexp) {
   121  	for pattern, mappingsOfURI := range bm.m.patternPath { // matching for pattern path
   122  		if pattern.MatchString(bm.uri) {
   123  			pm = mappingsOfURI
   124  			pp = pattern
   125  		} else if bm.conf.MatchTrailingSlash && pattern.MatchString(bm.uri+"/") {
   126  			bm.uri += "/"
   127  			pm = mappingsOfURI
   128  			pp = pattern
   129  		}
   130  	}
   131  	return
   132  }
   133  
   134  func (bm *boundMatcher) headMatches() bool {
   135  	return bm.matchState == matchHead
   136  }
   137  
   138  func (bm *boundMatcher) matchByMethod(mappings []*mckmaps.Mapping) *mckmaps.Mapping {
   139  	return matchByMethod(mappings, bm.method)
   140  }
   141  
   142  func (bm *boundMatcher) matchHead(mappings []*mckmaps.Mapping) *mckmaps.Mapping {
   143  	if bm.method != myhttp.MethodHead {
   144  		return nil
   145  	}
   146  
   147  	return matchByMethod(mappings, myhttp.MethodGet)
   148  }
   149  
   150  func (bm *boundMatcher) matchCORSOptions() bool {
   151  	return bm.conf.CORS.Enabled && bm.method == myhttp.MethodOptions
   152  }
   153  
   154  func matchByMethod(mappings []*mckmaps.Mapping, method myhttp.HTTPMethod) *mckmaps.Mapping {
   155  	for _, m := range mappings {
   156  		if m.Method.Matches(method) {
   157  			return m
   158  		}
   159  	}
   160  	return nil
   161  }
   162  
   163  func (bm *boundMatcher) matchPolicy() *mckmaps.Policy {
   164  	switch bm.matchState {
   165  	case matchHead:
   166  		fallthrough
   167  	case matchExact:
   168  		p := bm.matchExactPolicy()
   169  		if p == nil {
   170  			return pNoPolicyMatched
   171  		} else {
   172  			return p
   173  		}
   174  	case matchCORSOptions:
   175  		return pEmptyOK
   176  	case matchURI:
   177  		return pMethodNotAllowed
   178  	}
   179  	return pNotFound
   180  }
   181  
   182  func (bm *boundMatcher) matchExactPolicy() *mckmaps.Policy {
   183  	bm.cacheBody()
   184  
   185  	err := bm.r.ParseForm()
   186  	if err != nil {
   187  		log.Println("[server  ] fail to parse form:", err)
   188  		return nil
   189  	}
   190  
   191  	var policy *mckmaps.Policy
   192  	for _, p := range bm.matchedMapping.Policies {
   193  		when := p.When
   194  
   195  		if when != nil {
   196  			if bm.uriPattern != nil && !bm.pathVarsMatch(when) {
   197  				continue
   198  			}
   199  
   200  			if !bm.paramsMatch(when) {
   201  				continue
   202  			}
   203  
   204  			if !bm.headersMatch(when) {
   205  				continue
   206  			}
   207  
   208  			if !bm.bodyMatches(when) {
   209  				continue
   210  			}
   211  		}
   212  
   213  		policy = p
   214  		break
   215  	}
   216  
   217  	// resets body for later use in executor
   218  	bm.resetBodyFromCache()
   219  
   220  	return policy
   221  }
   222  
   223  func (bm *boundMatcher) cacheBody() {
   224  	body, err := ioutil.ReadAll(bm.r.Body)
   225  	if err == nil {
   226  		bm.bodyCache = body
   227  		bm.resetBodyFromCache()
   228  	}
   229  }
   230  
   231  func (bm *boundMatcher) resetBodyFromCache() {
   232  	if bm.bodyCache != nil {
   233  		bm.r.Body = ioutil.NopCloser(bytes.NewReader(bm.bodyCache))
   234  	}
   235  }
   236  
   237  func (bm *boundMatcher) pathVarsMatch(when *mckmaps.When) bool {
   238  	pathVars := bm.extractPathVars()
   239  
   240  	if !valuesMatch(when.PathVars, pathVars) {
   241  		return false
   242  	}
   243  	if !regexpsMatch(when.PathVarRegexps, pathVars) {
   244  		return false
   245  	}
   246  
   247  	return true
   248  }
   249  
   250  func (bm *boundMatcher) extractPathVars() map[string][]string {
   251  	mValues := bm.uriPattern.FindStringSubmatch(bm.uri)
   252  	if len(mValues) == 0 {
   253  		panic("Shouldn't happen")
   254  	}
   255  	mNames := bm.uriPattern.SubexpNames()
   256  	pathVars := make(map[string][]string, len(mValues))
   257  	for i := 1; i < len(mValues); i++ {
   258  		pathVars[mNames[i][1:]] = []string{mValues[i]} // [1:] to remove the prefix v
   259  	}
   260  	return pathVars
   261  }
   262  
   263  func (bm *boundMatcher) paramsMatch(when *mckmaps.When) bool {
   264  	if !valuesMatch(when.Params, bm.r.Form) {
   265  		return false
   266  	}
   267  	if !regexpsMatch(when.ParamRegexps, bm.r.Form) {
   268  		return false
   269  	}
   270  	if !asJSONsMatch(when.ParamJSONs, bm.r.Form) {
   271  		return false
   272  	}
   273  
   274  	return true
   275  }
   276  
   277  func (bm *boundMatcher) headersMatch(when *mckmaps.When) bool {
   278  	if !valuesMatch(when.Headers, bm.r.Header) {
   279  		return false
   280  	}
   281  	if !regexpsMatch(when.HeaderRegexps, bm.r.Header) {
   282  		return false
   283  	}
   284  	if !asJSONsMatch(when.HeaderJSONs, bm.r.Header) {
   285  		return false
   286  	}
   287  
   288  	return true
   289  }
   290  
   291  func (bm *boundMatcher) bodyMatches(when *mckmaps.When) bool {
   292  	body := bm.bodyCache
   293  	if when.Body != nil {
   294  		return bytes.Equal(when.Body, body)
   295  	} else if when.BodyRegexp != nil {
   296  		return when.BodyRegexp.Match(body)
   297  	} else if when.BodyJSON != nil {
   298  		json, err := myjson.Unmarshal(body)
   299  		if err != nil {
   300  			return false
   301  		}
   302  		return when.BodyJSON.Matches(json)
   303  	} else {
   304  		return true
   305  	}
   306  }
   307  
   308  func valuesMatch(expected []*mckmaps.NameValuesPair, actual map[string][]string) bool {
   309  	for _, e := range expected {
   310  		formValues := actual[e.Name]
   311  
   312  		if !stringSlicesEqualIgnoreOrder(e.Values, formValues) {
   313  			return false
   314  		}
   315  	}
   316  
   317  	return true
   318  }
   319  
   320  // tests if two []string share same elements, ignoring the order
   321  func stringSlicesEqualIgnoreOrder(l, r []string) bool {
   322  	if len(l) != len(r) {
   323  		return false
   324  	}
   325  
   326  	diff := make(map[string]int, len(l))
   327  	for _, _x := range l {
   328  		diff[_x]++
   329  	}
   330  
   331  	for _, _y := range r {
   332  		if _, ok := diff[_y]; !ok {
   333  			return false
   334  		}
   335  
   336  		diff[_y] -= 1
   337  		if diff[_y] == 0 {
   338  			delete(diff, _y)
   339  		}
   340  	}
   341  
   342  	return len(diff) == 0
   343  }
   344  
   345  func regexpsMatch(expected []*mckmaps.NameRegexpPair, actual map[string][]string) bool {
   346  	for _, e := range expected {
   347  		formValues := actual[e.Name]
   348  
   349  		if !regexpAnyMatches(e.Regexp, formValues) {
   350  			return false
   351  		}
   352  	}
   353  
   354  	return true
   355  }
   356  
   357  func regexpAnyMatches(r *regexp.Regexp, values []string) bool {
   358  	for _, v := range values {
   359  		if r.Match([]byte(v)) {
   360  			return true
   361  		}
   362  	}
   363  	return false
   364  }
   365  
   366  func asJSONsMatch(expected []*mckmaps.NameJSONPair, actual map[string][]string) bool {
   367  	for _, e := range expected {
   368  		formValues := actual[e.Name]
   369  
   370  		if !asJSONAnyMatches(e.JSON, formValues) {
   371  			return false
   372  		}
   373  	}
   374  
   375  	return true
   376  }
   377  
   378  func asJSONAnyMatches(m myjson.ExtJSONMatcher, values []string) bool {
   379  	for _, v := range values {
   380  		json, err := myjson.Unmarshal([]byte(v))
   381  		if err != nil {
   382  			continue
   383  		}
   384  
   385  		if m.Matches(json) {
   386  			return true
   387  		}
   388  	}
   389  	return false
   390  }