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 }