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 }