github.com/kumasuke120/mockuma@v1.1.9/internal/mckmaps/parser.go (about)

     1  package mckmaps
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/kumasuke120/mockuma/internal/myhttp"
    11  	"github.com/kumasuke120/mockuma/internal/myjson"
    12  	"github.com/kumasuke120/mockuma/internal/myos"
    13  	"github.com/kumasuke120/mockuma/internal/types"
    14  	"github.com/rs/cors"
    15  )
    16  
    17  type MockuMappings struct {
    18  	Mappings  []*Mapping
    19  	Filenames []string
    20  	Config    *Config
    21  }
    22  
    23  func (m *MockuMappings) IsEmpty() bool {
    24  	return len(m.Mappings) == 0 && len(m.Filenames) == 0
    25  }
    26  
    27  func (m *MockuMappings) GroupMethodsByURI() map[string][]myhttp.HTTPMethod {
    28  	result := make(map[string][]myhttp.HTTPMethod)
    29  	for _, m := range m.Mappings {
    30  		mappingsOfURI := result[m.URI]
    31  		mappingsOfURI = append(mappingsOfURI, m.Method)
    32  		result[m.URI] = mappingsOfURI
    33  	}
    34  	return result
    35  }
    36  
    37  type Config struct {
    38  	CORS               *CORSOptions
    39  	MatchTrailingSlash bool
    40  }
    41  
    42  func defaultConfig() *Config {
    43  	return &Config{
    44  		CORS:               defaultDisabledCORS(),
    45  		MatchTrailingSlash: false,
    46  	}
    47  }
    48  
    49  type CORSOptions struct {
    50  	Enabled          bool
    51  	AllowCredentials bool
    52  	MaxAge           int64
    53  	AllowedOrigins   []string
    54  	AllowedMethods   []myhttp.HTTPMethod
    55  	AllowedHeaders   []string
    56  	ExposedHeaders   []string
    57  }
    58  
    59  func defaultEnabledCORS() *CORSOptions {
    60  	return &CORSOptions{
    61  		Enabled:          true,
    62  		AllowCredentials: true,
    63  		MaxAge:           1800,
    64  		AllowedOrigins:   []string{"*"},
    65  		AllowedMethods: []myhttp.HTTPMethod{
    66  			myhttp.MethodGet,
    67  			myhttp.MethodPost,
    68  			myhttp.MethodHead,
    69  			myhttp.MethodOptions,
    70  		},
    71  		AllowedHeaders: []string{
    72  			myhttp.HeaderOrigin,
    73  			myhttp.HeaderAccept,
    74  			myhttp.HeaderXRequestWith,
    75  			myhttp.HeaderContentType,
    76  			myhttp.HeaderAccessControlRequestMethod,
    77  			myhttp.HeaderAccessControlRequestHeaders,
    78  		},
    79  		ExposedHeaders: nil,
    80  	}
    81  }
    82  
    83  func defaultDisabledCORS() *CORSOptions {
    84  	return &CORSOptions{Enabled: false}
    85  }
    86  
    87  var anyStrToTrue = func(string) bool { return true }
    88  
    89  func (c *CORSOptions) ToCors() *cors.Cors {
    90  	if c.Enabled {
    91  		ac := c.AllowedMethods
    92  		if !myhttp.MethodsAnyMatches(ac, myhttp.MethodOptions) { // always allows OPTIONS
    93  			ac = append(ac, myhttp.MethodOptions)
    94  		}
    95  
    96  		// makes github.com/rs/cors returns the Origin of a request
    97  		// as the value of response header Access-Control-Allow-Origin
    98  		// when Access-Control-Allow-Credentials is 'true' and all
    99  		// origins are allowed
   100  		var ao []string
   101  		var aof func(string) bool
   102  		if c.allowsAllOrigins() {
   103  			if c.AllowCredentials {
   104  				ao = nil
   105  				aof = anyStrToTrue
   106  			} else {
   107  				ao = []string{"*"}
   108  				aof = nil
   109  			}
   110  		}
   111  
   112  		return cors.New(cors.Options{
   113  			AllowCredentials: c.AllowCredentials,
   114  			MaxAge:           int(c.MaxAge),
   115  			AllowedOrigins:   ao,
   116  			AllowOriginFunc:  aof,
   117  			AllowedMethods:   myhttp.MethodsToStringSlice(ac),
   118  			AllowedHeaders:   c.AllowedHeaders,
   119  			ExposedHeaders:   c.ExposedHeaders,
   120  		})
   121  	}
   122  
   123  	return nil
   124  }
   125  
   126  func (c *CORSOptions) allowsAllOrigins() bool {
   127  	if len(c.AllowedOrigins) == 0 {
   128  		return true
   129  	}
   130  	for _, o := range c.AllowedOrigins {
   131  		if "*" == o { // allows all origins
   132  			return true
   133  		}
   134  	}
   135  	return false
   136  }
   137  
   138  var loadedFilenames []string
   139  
   140  func recordLoadedFile(name string) {
   141  	loadedFilenames = append(loadedFilenames, name)
   142  }
   143  
   144  type loadError struct {
   145  	filename string
   146  	err      error
   147  }
   148  
   149  func indentErrorMsg(err error) string {
   150  	errMsg := err.Error()
   151  	errMsg = strings.ReplaceAll(errMsg, "\n", "\n\t")
   152  	return errMsg
   153  }
   154  
   155  func (e *loadError) Error() string {
   156  	return fmt.Sprintf("cannot load the file '%s': \n\t%s",
   157  		e.filename, indentErrorMsg(e.err))
   158  }
   159  
   160  type parserError struct {
   161  	filename string
   162  	jsonPath *myjson.Path
   163  	err      error
   164  }
   165  
   166  func (e *parserError) Error() string {
   167  	result := ""
   168  	if e.jsonPath == nil {
   169  		result += "cannot parse json data"
   170  	} else {
   171  		result += fmt.Sprintf("cannot parse the value on json-path \"%v\"", e.jsonPath)
   172  	}
   173  
   174  	if e.filename != "" {
   175  		result += fmt.Sprintf(" in the file '%s'", e.filename)
   176  	}
   177  
   178  	if e.err != nil {
   179  		result += ": \n\t" + indentErrorMsg(e.err)
   180  	}
   181  
   182  	return result
   183  }
   184  
   185  // preprocessors singletons
   186  var (
   187  	ppRemoveComment  = &dCommentProcessor{}
   188  	ppRenderTemplate = makeDTemplateProcessor()
   189  	ppLoadFile       = makeDFileProcessor()
   190  	ppParseRegexp    = makeDRegexpProcessor()
   191  	ppToJSONMatcher  = &dJSONProcessor{}
   192  )
   193  
   194  type Parser struct {
   195  	filename string
   196  }
   197  
   198  func NewParser(filename string) *Parser {
   199  	return &Parser{filename: filename}
   200  }
   201  
   202  func (p *Parser) newJSONParseError(jsonPath *myjson.Path) *parserError {
   203  	return &parserError{filename: p.filename, jsonPath: jsonPath}
   204  }
   205  
   206  func (p *Parser) Parse() (r *MockuMappings, e error) {
   207  	defer p.reset()
   208  
   209  	var json interface{}
   210  	if json, e = p.load(true, ppRemoveComment, ppRenderTemplate); e != nil {
   211  		return
   212  	}
   213  
   214  	switch json.(type) {
   215  	case myjson.Object: // parses in multi-file mode
   216  		parser := &mainParser{json: json.(myjson.Object), Parser: *p}
   217  		r, e = parser.parse()
   218  	case myjson.Array: // parses in single-file mode
   219  		parser := &mappingsParser{json: json, Parser: *p}
   220  		mappings, _err := parser.parse()
   221  		if _err == nil {
   222  			r, e = &MockuMappings{Mappings: mappings, Config: defaultConfig()}, _err
   223  		} else {
   224  			r, e = nil, _err
   225  		}
   226  	default:
   227  		r, e = nil, p.newJSONParseError(nil)
   228  	}
   229  
   230  	if r != nil {
   231  		relPaths, err := p.allRelative(loadedFilenames)
   232  		if err != nil {
   233  			return nil, err
   234  		}
   235  		r.Filenames = relPaths
   236  	}
   237  
   238  	p.sortMappings(r)
   239  	return
   240  }
   241  
   242  func (p *Parser) load(record bool, preprocessors ...types.Filter) (interface{}, error) {
   243  	if err := checkFilepath(p.filename); err != nil {
   244  		return nil, &loadError{filename: p.filename, err: err}
   245  	}
   246  
   247  	bytes, err := ioutil.ReadFile(p.filename)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	json, err := myjson.Unmarshal(bytes)
   253  	if err != nil {
   254  		return nil, p.newJSONParseError(nil)
   255  	}
   256  
   257  	v, err := types.DoFiltersOnV(json, preprocessors...) // runs given preprocessors
   258  	if err != nil {
   259  		return nil, &loadError{filename: p.filename, err: err}
   260  	}
   261  
   262  	if record {
   263  		recordLoadedFile(p.filename)
   264  	}
   265  	return v, nil
   266  }
   267  
   268  func (p *Parser) allRelative(filenames []string) (ret []string, err error) {
   269  	wd := myos.GetWd()
   270  
   271  	ret = make([]string, len(filenames))
   272  	for i, p := range filenames {
   273  		rp := p
   274  		if filepath.IsAbs(p) {
   275  			rp, err = filepath.Rel(wd, p)
   276  			if err != nil {
   277  				ret = nil
   278  				return
   279  			}
   280  		}
   281  
   282  		ret[i] = rp
   283  	}
   284  	return
   285  }
   286  
   287  func (p *Parser) reset() {
   288  	ppRenderTemplate.reset()
   289  	ppLoadFile.reset()
   290  	ppParseRegexp.reset()
   291  
   292  	loadedFilenames = nil
   293  	parsingTemplates = nil
   294  }
   295  
   296  func (p *Parser) sortMappings(mappings *MockuMappings) {
   297  	if mappings == nil {
   298  		return
   299  	}
   300  
   301  	uri2mappings := make(map[string][]*Mapping)
   302  	var uriOrder []string
   303  	uriOrderContains := make(map[string]bool)
   304  	for _, m := range mappings.Mappings {
   305  		mappingsOfURI := uri2mappings[m.URI]
   306  
   307  		mappingsOfURI = appendToMappingsOfURI(mappingsOfURI, m)
   308  		uri2mappings[m.URI] = mappingsOfURI
   309  		if _, ok := uriOrderContains[m.URI]; !ok {
   310  			uriOrderContains[m.URI] = true
   311  			uriOrder = append(uriOrder, m.URI)
   312  		}
   313  	}
   314  	ms := make([]*Mapping, 0, len(mappings.Mappings))
   315  	for _, uri := range uriOrder {
   316  		mappingsOfURI := uri2mappings[uri]
   317  		ms = append(ms, mappingsOfURI...)
   318  	}
   319  
   320  	mappings.Mappings = ms
   321  }
   322  
   323  func appendToMappingsOfURI(dst []*Mapping, m *Mapping) []*Mapping {
   324  	merged := false
   325  	for _, dm := range dst {
   326  		if dm.URI == m.URI && dm.Method == m.Method {
   327  			dm.Policies = append(dm.Policies, m.Policies...)
   328  			merged = true
   329  		}
   330  	}
   331  
   332  	if !merged {
   333  		dst = append(dst, m)
   334  	}
   335  	return dst
   336  }
   337  
   338  type mainParser struct {
   339  	json     myjson.Object
   340  	jsonPath *myjson.Path
   341  	Parser
   342  }
   343  
   344  func (p *mainParser) parse() (*MockuMappings, error) {
   345  	p.jsonPath = myjson.NewPath("")
   346  
   347  	p.jsonPath.SetLast(aType)
   348  	_type, err := p.json.GetString(aType)
   349  	if err != nil || string(_type) != tMain {
   350  		return nil, p.newJSONParseError(p.jsonPath)
   351  	}
   352  
   353  	p.jsonPath.SetLast(aInclude)
   354  	mappings, err := p.parseInclude(err)
   355  	if err != nil {
   356  		return nil, err
   357  	}
   358  
   359  	p.jsonPath.SetLast(aConfig)
   360  	rawConf := p.json.Get(aConfig)
   361  	cc, err := p.parseConfig(rawConf)
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  
   366  	return &MockuMappings{Mappings: mappings, Config: cc}, nil
   367  }
   368  
   369  func (p *mainParser) parseInclude(err error) ([]*Mapping, error) {
   370  	include, err := p.json.GetObject(aInclude)
   371  	if err != nil {
   372  		return nil, p.newJSONParseError(p.jsonPath)
   373  	}
   374  
   375  	p.jsonPath.Append(tMappings)
   376  	filenamesOfMappings, err := include.GetArray(tMappings)
   377  	if err != nil {
   378  		return nil, p.newJSONParseError(p.jsonPath)
   379  	}
   380  
   381  	p.jsonPath.Append("")
   382  	var mappings []*Mapping
   383  	for idx, filename := range filenamesOfMappings {
   384  		p.jsonPath.SetLast(idx)
   385  
   386  		_filename, err := myjson.ToString(filename)
   387  		if err != nil {
   388  			return nil, p.newJSONParseError(p.jsonPath)
   389  		}
   390  
   391  		f := string(_filename)
   392  		glob, err := filepath.Glob(f)
   393  		if err != nil {
   394  			return nil, p.newJSONParseError(myjson.NewPath(aInclude, tMappings, idx))
   395  		}
   396  
   397  		for _, g := range glob {
   398  			parser := &mappingsParser{Parser: Parser{filename: g}}
   399  			partOfMappings, err := parser.parse() // parses mappings for each included file
   400  			if err != nil {
   401  				return nil, err
   402  			}
   403  
   404  			mappings = append(mappings, partOfMappings...)
   405  		}
   406  
   407  		recordLoadedFile(f)
   408  	}
   409  	p.jsonPath.RemoveLast()
   410  	p.jsonPath.RemoveLast()
   411  
   412  	return mappings, nil
   413  }
   414  
   415  func (p *mainParser) parseConfig(v interface{}) (c *Config, err error) {
   416  	switch v.(type) {
   417  	case nil:
   418  		c = defaultConfig()
   419  	case myjson.Object:
   420  		vo := v.(myjson.Object)
   421  		p.jsonPath.Append("")
   422  
   423  		p.jsonPath.SetLast(aConfigCORS)
   424  		var co *CORSOptions
   425  		co, err = p.parseCORSOptions(vo)
   426  		if err != nil {
   427  			return
   428  		}
   429  
   430  		p.jsonPath.SetLast(aConfigMatchTrailingSlash)
   431  		var mts bool
   432  		if vo.Has(aConfigMatchTrailingSlash) {
   433  			_mts, _err := vo.GetBoolean(aConfigMatchTrailingSlash)
   434  			if _err != nil {
   435  				err = p.newJSONParseError(p.jsonPath)
   436  				return
   437  			}
   438  			mts = bool(_mts)
   439  		} else {
   440  			mts = false
   441  		}
   442  
   443  		c = &Config{CORS: co, MatchTrailingSlash: mts}
   444  		p.jsonPath.RemoveLast()
   445  	default:
   446  		return nil, p.newJSONParseError(p.jsonPath)
   447  	}
   448  
   449  	return
   450  }
   451  
   452  func (p *mainParser) parseCORSOptions(v myjson.Object) (co *CORSOptions, err error) {
   453  	_co := v.Get(aConfigCORS)
   454  	switch _co.(type) {
   455  	case nil:
   456  		co = defaultDisabledCORS()
   457  	case myjson.Boolean:
   458  		if _co.(myjson.Boolean) {
   459  			co = defaultEnabledCORS()
   460  		} else {
   461  			co = defaultDisabledCORS()
   462  		}
   463  	case myjson.Object:
   464  		_corsV := _co.(myjson.Object)
   465  		p.jsonPath.Append("")
   466  		co, err = p.parseActualCORSOptions(_corsV)
   467  		if err != nil {
   468  			return
   469  		}
   470  		p.jsonPath.RemoveLast()
   471  	default:
   472  		err = p.newJSONParseError(p.jsonPath)
   473  		return
   474  	}
   475  	return
   476  }
   477  
   478  func (p *mainParser) parseActualCORSOptions(v myjson.Object) (*CORSOptions, error) {
   479  	p.jsonPath.SetLast(corsEnabled)
   480  	enabled, err := v.GetBoolean(corsEnabled)
   481  	if err != nil {
   482  		return nil, p.newJSONParseError(p.jsonPath)
   483  	}
   484  
   485  	if enabled {
   486  		cc := defaultEnabledCORS()
   487  
   488  		if v.Has(corsAllowCredentials) {
   489  			p.jsonPath.SetLast(corsAllowCredentials)
   490  			ac, err := v.GetBoolean(corsAllowCredentials)
   491  			if err != nil {
   492  				return nil, p.newJSONParseError(p.jsonPath)
   493  			}
   494  			cc.AllowCredentials = bool(ac)
   495  		}
   496  
   497  		if v.Has(corsMaxAge) {
   498  			p.jsonPath.SetLast(corsMaxAge)
   499  			ma, err := v.GetNumber(corsMaxAge)
   500  			if err != nil {
   501  				return nil, p.newJSONParseError(p.jsonPath)
   502  			}
   503  			cc.MaxAge = int64(ma)
   504  		}
   505  
   506  		if v.Has(corsAllowedOrigins) {
   507  			p.jsonPath.SetLast(corsAllowedOrigins)
   508  			ao, err := p.getAsStringSlice(v, corsAllowedOrigins)
   509  			if err != nil {
   510  				return nil, err
   511  			}
   512  			cc.AllowedOrigins = ao
   513  		}
   514  
   515  		if v.Has(corsAllowedMethods) {
   516  			p.jsonPath.SetLast(corsAllowedMethods)
   517  			_am, err := p.getAsStringSlice(v, corsAllowedMethods)
   518  			if err != nil {
   519  				return nil, err
   520  			}
   521  			am := make([]myhttp.HTTPMethod, len(_am))
   522  			for idx, v := range _am {
   523  				am[idx] = myhttp.ToHTTPMethod(v)
   524  			}
   525  			cc.AllowedMethods = am
   526  		}
   527  
   528  		if v.Has(corsAllowedHeaders) {
   529  			p.jsonPath.SetLast(corsAllowedHeaders)
   530  			ah, err := p.getAsStringSlice(v, corsAllowedHeaders)
   531  			if err != nil {
   532  				return nil, err
   533  			}
   534  			cc.AllowedHeaders = ah
   535  		}
   536  
   537  		if v.Has(corsExposedHeaders) {
   538  			p.jsonPath.SetLast(corsExposedHeaders)
   539  			eh, err := p.getAsStringSlice(v, corsExposedHeaders)
   540  			if err != nil {
   541  				return nil, err
   542  			}
   543  			cc.ExposedHeaders = eh
   544  		}
   545  
   546  		return cc, nil
   547  	}
   548  
   549  	return defaultDisabledCORS(), nil
   550  }
   551  
   552  func (p *mainParser) getAsStringSlice(v myjson.Object, name string) ([]string, error) {
   553  	p.jsonPath.Append("")
   554  
   555  	var result []string
   556  	for idx, e := range ensureJSONArray(v.Get(name)) {
   557  		p.jsonPath.SetLast(idx)
   558  
   559  		s, err := myjson.ToString(e)
   560  		if err != nil {
   561  			return nil, p.newJSONParseError(p.jsonPath)
   562  		}
   563  		result = append(result, string(s))
   564  	}
   565  	p.jsonPath.RemoveLast()
   566  
   567  	return result, nil
   568  }
   569  
   570  func ensureJSONArray(v interface{}) myjson.Array {
   571  	switch v.(type) {
   572  	case myjson.Array:
   573  		return v.(myjson.Array)
   574  	default:
   575  		return myjson.NewArray(v)
   576  	}
   577  }
   578  
   579  func checkFilepath(path string) (err error) {
   580  	wd := myos.GetWd()
   581  
   582  	relPath := path
   583  	if filepath.IsAbs(path) {
   584  		relPath, err = filepath.Rel(wd, path)
   585  		if err != nil {
   586  			return
   587  		}
   588  	}
   589  
   590  	if strings.HasPrefix(relPath, "..") { // paths should be under the current working directory
   591  		return errors.New("included file isn't in the current working directory")
   592  	}
   593  	return
   594  }