github.com/Tyktechnologies/tyk@v2.9.5+incompatible/gateway/mw_js_plugin.go (about) 1 package gateway 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "encoding/base64" 7 "encoding/json" 8 "errors" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "os" 13 "path/filepath" 14 "reflect" 15 "strings" 16 "sync" 17 "time" 18 19 "github.com/robertkrimen/otto" 20 _ "github.com/robertkrimen/otto/underscore" 21 22 "github.com/TykTechnologies/tyk/config" 23 "github.com/TykTechnologies/tyk/user" 24 25 "github.com/sirupsen/logrus" 26 ) 27 28 // Lets the user override and return a response from middleware 29 type ReturnOverrides struct { 30 ResponseCode int 31 ResponseError string 32 ResponseBody string 33 ResponseHeaders map[string]string 34 OverrideError bool 35 } 36 37 // MiniRequestObject is marshalled to JSON string and passed into JSON middleware 38 type MiniRequestObject struct { 39 Headers map[string][]string 40 SetHeaders map[string]string 41 DeleteHeaders []string 42 Body []byte 43 URL string 44 Params map[string][]string 45 AddParams map[string]string 46 ExtendedParams map[string][]string 47 DeleteParams []string 48 ReturnOverrides ReturnOverrides 49 IgnoreBody bool 50 Method string 51 RequestURI string 52 Scheme string 53 } 54 55 func (mr *MiniRequestObject) ReconstructParams(r *http.Request) { 56 updatedValues := r.URL.Query() 57 58 for _, k := range mr.DeleteParams { 59 updatedValues.Del(k) 60 } 61 62 for p, v := range mr.AddParams { 63 updatedValues.Set(p, v) 64 } 65 66 for p, v := range mr.ExtendedParams { 67 for _, val := range v { 68 updatedValues.Add(p, val) 69 } 70 } 71 72 if !reflect.DeepEqual(r.URL.Query(), updatedValues) { 73 r.URL.RawQuery = updatedValues.Encode() 74 } 75 } 76 77 type VMReturnObject struct { 78 Request MiniRequestObject 79 SessionMeta map[string]string 80 Session user.SessionState 81 AuthValue string 82 } 83 84 // DynamicMiddleware is a generic middleware that will execute JS code before continuing 85 type DynamicMiddleware struct { 86 BaseMiddleware 87 MiddlewareClassName string 88 Pre bool 89 UseSession bool 90 Auth bool 91 } 92 93 func (d *DynamicMiddleware) Name() string { 94 return "DynamicMiddleware" 95 } 96 97 func specToJson(spec *APISpec) string { 98 m := map[string]interface{}{ 99 "OrgID": spec.OrgID, 100 "APIID": spec.APIID, 101 // For backwards compatibility within 2.x. 102 // TODO: simplify or refactor in 3.x or later. 103 "config_data": spec.ConfigData, 104 } 105 bs, err := json.Marshal(m) 106 if err != nil { 107 log.Error("Failed to encode configuration data: ", err) 108 return "" 109 } 110 return string(bs) 111 } 112 113 // ProcessRequest will run any checks on the request on the way through the system, return an error to have the chain fail 114 func (d *DynamicMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) { 115 t1 := time.Now().UnixNano() 116 logger := d.Logger() 117 118 // Create the proxy object 119 originalBody, err := ioutil.ReadAll(r.Body) 120 if err != nil { 121 logger.WithError(err).Error("Failed to read request body") 122 return nil, http.StatusOK 123 } 124 defer r.Body.Close() 125 126 headers := r.Header 127 host := r.Host 128 if host == "" && r.URL != nil { 129 host = r.URL.Host 130 } 131 if host != "" { 132 headers = make(http.Header) 133 for k, v := range r.Header { 134 headers[k] = v 135 } 136 headers.Set("Host", host) 137 } 138 scheme := "http" 139 if r.TLS != nil { 140 scheme = "https" 141 } 142 143 requestData := MiniRequestObject{ 144 Headers: headers, 145 SetHeaders: map[string]string{}, 146 DeleteHeaders: []string{}, 147 Body: originalBody, 148 URL: r.URL.String(), 149 Params: r.URL.Query(), 150 AddParams: map[string]string{}, 151 ExtendedParams: map[string][]string{}, 152 DeleteParams: []string{}, 153 Method: r.Method, 154 RequestURI: r.RequestURI, 155 Scheme: scheme, 156 } 157 158 requestAsJson, err := json.Marshal(requestData) 159 if err != nil { 160 logger.WithError(err).Error("Failed to encode request object for dynamic middleware") 161 return nil, http.StatusOK 162 } 163 164 specAsJson := specToJson(d.Spec) 165 166 session := new(user.SessionState) 167 session.Mutex = &sync.RWMutex{} 168 // Encode the session object (if not a pre-process) 169 if !d.Pre && d.UseSession { 170 session = ctxGetSession(r) 171 } 172 173 sessionAsJson, err := json.Marshal(session) 174 if err != nil { 175 logger.WithError(err).Error("Failed to encode session for VM") 176 return nil, http.StatusOK 177 } 178 179 // Run the middleware 180 middlewareClassname := d.MiddlewareClassName 181 vm := d.Spec.JSVM.VM.Copy() 182 vm.Interrupt = make(chan func(), 1) 183 logger.Debug("Running: ", middlewareClassname) 184 // buffered, leaving no chance of a goroutine leak since the 185 // spawned goroutine will send 0 or 1 values. 186 ret := make(chan otto.Value, 1) 187 errRet := make(chan error, 1) 188 go func() { 189 defer func() { 190 // the VM executes the panic func that gets it 191 // to stop, so we must recover here to not crash 192 // the whole Go program. 193 recover() 194 }() 195 returnRaw, err := vm.Run(middlewareClassname + `.DoProcessRequest(` + string(requestAsJson) + `, ` + string(sessionAsJson) + `, ` + specAsJson + `);`) 196 ret <- returnRaw 197 errRet <- err 198 }() 199 var returnRaw otto.Value 200 t := time.NewTimer(d.Spec.JSVM.Timeout) 201 select { 202 case returnRaw = <-ret: 203 if err := <-errRet; err != nil { 204 logger.WithError(err).Error("Failed to run JS middleware") 205 return nil, http.StatusOK 206 } 207 t.Stop() 208 case <-t.C: 209 t.Stop() 210 logger.Error("JS middleware timed out after ", d.Spec.JSVM.Timeout) 211 vm.Interrupt <- func() { 212 // only way to stop the VM is to send it a func 213 // that panics. 214 panic("stop") 215 } 216 return nil, http.StatusOK 217 } 218 returnDataStr, _ := returnRaw.ToString() 219 220 // Decode the return object 221 newRequestData := VMReturnObject{} 222 if err := json.Unmarshal([]byte(returnDataStr), &newRequestData); err != nil { 223 logger.WithError(err).Error("Failed to decode middleware request data on return from VM. Returned data: ", returnDataStr) 224 return nil, http.StatusOK 225 } 226 227 // Reconstruct the request parts 228 if newRequestData.Request.IgnoreBody { 229 r.ContentLength = int64(len(originalBody)) 230 r.Body = ioutil.NopCloser(bytes.NewReader(originalBody)) 231 } else { 232 r.ContentLength = int64(len(newRequestData.Request.Body)) 233 r.Body = ioutil.NopCloser(bytes.NewReader(newRequestData.Request.Body)) 234 } 235 236 r.URL, err = url.Parse(newRequestData.Request.URL) 237 if err != nil { 238 return nil, http.StatusOK 239 } 240 241 // Delete and set headers 242 for _, dh := range newRequestData.Request.DeleteHeaders { 243 r.Header.Del(dh) 244 } 245 246 for h, v := range newRequestData.Request.SetHeaders { 247 r.Header.Set(h, v) 248 } 249 250 // Delete and set request parameters 251 newRequestData.Request.ReconstructParams(r) 252 253 // Save the session data (if modified) 254 if !d.Pre && d.UseSession { 255 newMeta := mapStrsToIfaces(newRequestData.SessionMeta) 256 if !reflect.DeepEqual(session.GetMetaData(), newMeta) { 257 session.SetMetaData(newMeta) 258 ctxScheduleSessionUpdate(r) 259 } 260 } 261 262 logger.Debug("JSVM middleware execution took: (ns) ", time.Now().UnixNano()-t1) 263 264 if newRequestData.Request.ReturnOverrides.ResponseError != "" { 265 newRequestData.Request.ReturnOverrides.ResponseBody = newRequestData.Request.ReturnOverrides.ResponseError 266 } 267 268 if newRequestData.Request.ReturnOverrides.ResponseCode >= http.StatusBadRequest && !newRequestData.Request.ReturnOverrides.OverrideError { 269 270 for header, value := range newRequestData.Request.ReturnOverrides.ResponseHeaders { 271 w.Header().Set(header, value) 272 } 273 274 return errors.New(newRequestData.Request.ReturnOverrides.ResponseBody), newRequestData.Request.ReturnOverrides.ResponseCode 275 } 276 277 if newRequestData.Request.ReturnOverrides.ResponseCode != 0 { 278 responseObject := VMResponseObject{ 279 Response: ResponseObject{ 280 Body: newRequestData.Request.ReturnOverrides.ResponseBody, 281 Code: newRequestData.Request.ReturnOverrides.ResponseCode, 282 Headers: newRequestData.Request.ReturnOverrides.ResponseHeaders, 283 }, 284 } 285 286 forceResponse(w, r, &responseObject, d.Spec, session, d.Pre, logger) 287 return nil, mwStatusRespond 288 } 289 290 if d.Auth { 291 ctxSetSession(r, &newRequestData.Session, newRequestData.AuthValue, true) 292 } 293 294 return nil, http.StatusOK 295 } 296 297 func mapStrsToIfaces(m map[string]string) map[string]interface{} { 298 // TODO: do we really need this conversion? note that we can't 299 // make user.SessionState.MetaData a map[string]string, however. 300 m2 := make(map[string]interface{}, len(m)) 301 for k, v := range m { 302 m2[k] = v 303 } 304 return m2 305 } 306 307 // --- Utility functions during startup to ensure a sane VM is present for each API Def ---- 308 309 type JSVM struct { 310 Spec *APISpec 311 VM *otto.Otto 312 Timeout time.Duration 313 Log *logrus.Entry // logger used by the JS code 314 RawLog *logrus.Logger // logger used by `rawlog` func to avoid formatting 315 } 316 317 const defaultJSVMTimeout = 5 318 319 // Init creates the JSVM with the core library and sets up a default 320 // timeout. 321 func (j *JSVM) Init(spec *APISpec, logger *logrus.Entry) { 322 vm := otto.New() 323 logger = logger.WithField("prefix", "jsvm") 324 325 // Init TykJS namespace, constructors etc. 326 if _, err := vm.Run(coreJS); err != nil { 327 logger.WithError(err).Error("Could not load TykJS") 328 return 329 } 330 331 // Load user's TykJS on top, if any 332 if path := config.Global().TykJSPath; path != "" { 333 f, err := os.Open(path) 334 if err == nil { 335 _, err = vm.Run(f) 336 f.Close() 337 338 if err != nil { 339 logger.WithError(err).Error("Could not load user's TykJS") 340 } 341 } 342 } 343 344 j.VM = vm 345 j.Spec = spec 346 347 // Add environment API 348 j.LoadTykJSApi() 349 350 if jsvmTimeout := config.Global().JSVMTimeout; jsvmTimeout <= 0 { 351 j.Timeout = time.Duration(defaultJSVMTimeout) * time.Second 352 logger.Debugf("Default JSVM timeout used: %v", j.Timeout) 353 } else { 354 j.Timeout = time.Duration(jsvmTimeout) * time.Second 355 logger.Debugf("Custom JSVM timeout: %v", j.Timeout) 356 } 357 358 j.Log = logger // use the global logger by default 359 j.RawLog = rawLog 360 } 361 362 // LoadJSPaths will load JS classes and functionality in to the VM by file 363 func (j *JSVM) LoadJSPaths(paths []string, prefix string) { 364 for _, mwPath := range paths { 365 if prefix != "" { 366 mwPath = filepath.Join(prefix, mwPath) 367 } 368 j.Log.Info("Loading JS File: ", mwPath) 369 f, err := os.Open(mwPath) 370 if err != nil { 371 j.Log.WithError(err).Error("Failed to open JS middleware file") 372 continue 373 } 374 if _, err := j.VM.Run(f); err != nil { 375 j.Log.WithError(err).Error("Failed to load JS middleware") 376 } 377 f.Close() 378 } 379 } 380 381 type TykJSHttpRequest struct { 382 Method string 383 Body string 384 Headers map[string]string 385 Domain string 386 Resource string 387 FormData map[string]string 388 } 389 390 type TykJSHttpResponse struct { 391 Code int 392 Body string 393 Headers map[string][]string 394 395 // Make this compatible with BatchReplyUnit 396 CodeComp int `json:"code"` 397 BodyComp string `json:"body"` 398 HeadersComp map[string][]string `json:"headers"` 399 } 400 401 func (j *JSVM) LoadTykJSApi() { 402 // Enable a log 403 j.VM.Set("log", func(call otto.FunctionCall) otto.Value { 404 j.Log.WithFields(logrus.Fields{ 405 "type": "log-msg", 406 }).Info(call.Argument(0).String()) 407 return otto.Value{} 408 }) 409 j.VM.Set("rawlog", func(call otto.FunctionCall) otto.Value { 410 j.RawLog.Print(call.Argument(0).String() + "\n") 411 return otto.Value{} 412 }) 413 414 // these two needed for non-utf8 bodies 415 j.VM.Set("b64dec", func(call otto.FunctionCall) otto.Value { 416 in := call.Argument(0).String() 417 out, err := base64.StdEncoding.DecodeString(in) 418 419 // Fallback to RawStdEncoding: 420 if err != nil { 421 out, err = base64.RawStdEncoding.DecodeString(in) 422 if err != nil { 423 j.Log.WithError(err).Error("Failed to base64 decode") 424 return otto.Value{} 425 } 426 } 427 returnVal, err := j.VM.ToValue(string(out)) 428 if err != nil { 429 j.Log.WithError(err).Error("Failed to base64 decode") 430 return otto.Value{} 431 } 432 return returnVal 433 }) 434 j.VM.Set("b64enc", func(call otto.FunctionCall) otto.Value { 435 in := []byte(call.Argument(0).String()) 436 out := base64.StdEncoding.EncodeToString(in) 437 returnVal, err := j.VM.ToValue(out) 438 if err != nil { 439 j.Log.WithError(err).Error("Failed to base64 encode") 440 return otto.Value{} 441 } 442 return returnVal 443 }) 444 445 j.VM.Set("rawb64dec", func(call otto.FunctionCall) otto.Value { 446 in := call.Argument(0).String() 447 out, err := base64.RawStdEncoding.DecodeString(in) 448 if err != nil { 449 if err != nil { 450 j.Log.WithError(err).Error("Failed to base64 decode") 451 return otto.Value{} 452 } 453 } 454 returnVal, err := j.VM.ToValue(string(out)) 455 if err != nil { 456 j.Log.WithError(err).Error("Failed to base64 decode") 457 return otto.Value{} 458 } 459 return returnVal 460 }) 461 j.VM.Set("rawb64enc", func(call otto.FunctionCall) otto.Value { 462 in := []byte(call.Argument(0).String()) 463 out := base64.RawStdEncoding.EncodeToString(in) 464 returnVal, err := j.VM.ToValue(out) 465 if err != nil { 466 j.Log.WithError(err).Error("Failed to base64 encode") 467 return otto.Value{} 468 } 469 return returnVal 470 }) 471 472 // Enable the creation of HTTP Requsts 473 j.VM.Set("TykMakeHttpRequest", func(call otto.FunctionCall) otto.Value { 474 jsonHRO := call.Argument(0).String() 475 if jsonHRO == "undefined" { 476 // Nope, return nothing 477 return otto.Value{} 478 } 479 hro := TykJSHttpRequest{} 480 if err := json.Unmarshal([]byte(jsonHRO), &hro); err != nil { 481 j.Log.WithError(err).Error("JSVM: Failed to deserialise HTTP Request object") 482 return otto.Value{} 483 } 484 485 // Make the request 486 domain := hro.Domain 487 data := url.Values{} 488 for k, v := range hro.FormData { 489 data.Set(k, v) 490 } 491 492 u, _ := url.ParseRequestURI(domain + hro.Resource) 493 urlStr := u.String() // "https://api.com/user/" 494 495 var d string 496 if hro.Body != "" { 497 d = hro.Body 498 } else if len(hro.FormData) > 0 { 499 d = data.Encode() 500 } 501 502 r, _ := http.NewRequest(hro.Method, urlStr, nil) 503 504 if d != "" { 505 r, _ = http.NewRequest(hro.Method, urlStr, strings.NewReader(d)) 506 } 507 508 for k, v := range hro.Headers { 509 r.Header.Set(k, v) 510 } 511 r.Close = true 512 513 tr := &http.Transport{TLSClientConfig: &tls.Config{}} 514 if cert := getUpstreamCertificate(r.Host, j.Spec); cert != nil { 515 tr.TLSClientConfig.Certificates = []tls.Certificate{*cert} 516 } 517 518 if config.Global().ProxySSLInsecureSkipVerify { 519 tr.TLSClientConfig.InsecureSkipVerify = true 520 } 521 522 if j.Spec.Proxy.Transport.SSLInsecureSkipVerify { 523 tr.TLSClientConfig.InsecureSkipVerify = true 524 } 525 526 tr.DialTLS = customDialTLSCheck(j.Spec, tr.TLSClientConfig) 527 528 tr.Proxy = proxyFromAPI(j.Spec) 529 530 // using new Client each time should be ok, since we closing connection every time 531 client := &http.Client{Transport: tr} 532 resp, err := client.Do(r) 533 if err != nil { 534 j.Log.WithError(err).Error("Request failed") 535 return otto.Value{} 536 } 537 538 body, _ := ioutil.ReadAll(resp.Body) 539 bodyStr := string(body) 540 tykResp := TykJSHttpResponse{ 541 Code: resp.StatusCode, 542 Body: bodyStr, 543 Headers: resp.Header, 544 CodeComp: resp.StatusCode, 545 BodyComp: bodyStr, 546 HeadersComp: resp.Header, 547 } 548 549 retAsStr, _ := json.Marshal(tykResp) 550 returnVal, err := j.VM.ToValue(string(retAsStr)) 551 if err != nil { 552 j.Log.WithError(err).Error("Failed to encode return value") 553 return otto.Value{} 554 } 555 556 return returnVal 557 }) 558 559 // Expose Setters and Getters in the REST API for a key: 560 j.VM.Set("TykGetKeyData", func(call otto.FunctionCall) otto.Value { 561 apiKey := call.Argument(0).String() 562 apiId := call.Argument(1).String() 563 564 obj, _ := handleGetDetail(apiKey, apiId, false) 565 bs, _ := json.Marshal(obj) 566 567 returnVal, err := j.VM.ToValue(string(bs)) 568 if err != nil { 569 j.Log.WithError(err).Error("Failed to encode return value") 570 return otto.Value{} 571 } 572 573 return returnVal 574 }) 575 576 j.VM.Set("TykSetKeyData", func(call otto.FunctionCall) otto.Value { 577 apiKey := call.Argument(0).String() 578 encoddedSession := call.Argument(1).String() 579 suppressReset := call.Argument(2).String() 580 581 newSession := user.SessionState{Mutex: &sync.RWMutex{}} 582 err := json.Unmarshal([]byte(encoddedSession), &newSession) 583 if err != nil { 584 j.Log.WithError(err).Error("Failed to decode the sesison data") 585 return otto.Value{} 586 } 587 588 doAddOrUpdate(apiKey, &newSession, suppressReset == "1", false) 589 590 return otto.Value{} 591 }) 592 593 // Batch request method 594 unsafeBatchHandler := BatchRequestHandler{} 595 j.VM.Set("TykBatchRequest", func(call otto.FunctionCall) otto.Value { 596 requestSet := call.Argument(0).String() 597 j.Log.Debug("Batch input is: ", requestSet) 598 599 bs, err := unsafeBatchHandler.ManualBatchRequest([]byte(requestSet)) 600 if err != nil { 601 j.Log.WithError(err).Error("Batch request error") 602 return otto.Value{} 603 } 604 605 returnVal, err := j.VM.ToValue(string(bs)) 606 if err != nil { 607 j.Log.WithError(err).Error("Failed to encode return value") 608 return otto.Value{} 609 } 610 611 return returnVal 612 }) 613 614 j.VM.Run(`function TykJsResponse(response, session_meta) { 615 return JSON.stringify({Response: response, SessionMeta: session_meta}) 616 }`) 617 } 618 619 const coreJS = ` 620 var TykJS = { 621 TykMiddleware: { 622 MiddlewareComponentMeta: function(configuration) { 623 this.configuration = configuration 624 } 625 }, 626 TykEventHandlers: { 627 EventHandlerComponentMeta: function() {} 628 } 629 } 630 631 TykJS.TykMiddleware.MiddlewareComponentMeta.prototype.ProcessRequest = function(request, session, config) { 632 log("Process Request Not Implemented") 633 return request 634 } 635 636 TykJS.TykMiddleware.MiddlewareComponentMeta.prototype.DoProcessRequest = function(request, session, config) { 637 request.Body = b64dec(request.Body) 638 var processed_request = this.ProcessRequest(request, session, config) 639 640 if (!processed_request) { 641 log("Middleware didn't return request object!") 642 return 643 } 644 645 // Reset the headers object 646 processed_request.Request.Headers = {} 647 processed_request.Request.Body = b64enc(processed_request.Request.Body) 648 649 return JSON.stringify(processed_request) 650 } 651 652 // The user-level middleware component 653 TykJS.TykMiddleware.NewMiddleware = function(configuration) { 654 TykJS.TykMiddleware.MiddlewareComponentMeta.call(this, configuration) 655 } 656 657 // Set up object inheritance 658 TykJS.TykMiddleware.NewMiddleware.prototype = Object.create(TykJS.TykMiddleware.MiddlewareComponentMeta.prototype) 659 TykJS.TykMiddleware.NewMiddleware.prototype.constructor = TykJS.TykMiddleware.NewMiddleware 660 661 TykJS.TykMiddleware.NewMiddleware.prototype.NewProcessRequest = function(callback) { 662 this.ProcessRequest = callback 663 } 664 665 TykJS.TykMiddleware.NewMiddleware.prototype.ReturnData = function(request, session) { 666 return {Request: request, SessionMeta: session} 667 } 668 669 TykJS.TykMiddleware.NewMiddleware.prototype.ReturnAuthData = function(request, session) { 670 return {Request: request, Session: session} 671 } 672 673 // Event Handler implementation 674 675 TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype.DoProcessEvent = function(event, context) { 676 // call the handler 677 log("Calling built - in handle") 678 this.Handle(event, context) 679 return 680 } 681 682 TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype.Handle = function(request, context) { 683 log("Handler not implemented!") 684 return request 685 } 686 687 // The user-level event handler component 688 TykJS.TykEventHandlers.NewEventHandler = function() { 689 TykJS.TykEventHandlers.EventHandlerComponentMeta.call(this) 690 } 691 692 // Set up object inheritance for events 693 TykJS.TykEventHandlers.NewEventHandler.prototype = Object.create(TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype) 694 TykJS.TykEventHandlers.NewEventHandler.prototype.constructor = TykJS.TykEventHandlers.NewEventHandler 695 696 TykJS.TykEventHandlers.NewEventHandler.prototype.NewHandler = function(callback) { 697 this.Handle = callback 698 };`