From 18a14b197d72301a41979e4c0e3b7618e62c9b20 Mon Sep 17 00:00:00 2001 From: joshblakeley Date: Fri, 2 Nov 2018 14:37:23 +0000 Subject: [PATCH 1/8] refactor gateway code to accept otto or goja as jsvm --- api_definition.go | 5 +- config/config.go | 1 + jsvm_event_handler.go | 2 +- jsvm_goja.go | 32 +++ jsvm_otto.go | 519 ++++++++++++++++++++++++++++++++++++ main.go | 2 +- mw_js_plugin.go | 450 +------------------------------ mw_virtual_endpoint.go | 75 ++---- mw_virtual_endpoint_test.go | 2 + 9 files changed, 588 insertions(+), 500 deletions(-) create mode 100644 jsvm_goja.go create mode 100644 jsvm_otto.go diff --git a/api_definition.go b/api_definition.go index 3c93ca6964b..8cd53216630 100644 --- a/api_definition.go +++ b/api_definition.go @@ -162,7 +162,7 @@ type APISpec struct { OrgSessionManager SessionHandler EventPaths map[apidef.TykEvent][]config.TykEventHandler Health HealthChecker - JSVM JSVM + JSVM TykJSVM ResponseChain []TykResponseHandler RoundRobin RoundRobin URLRewriteEnabled bool @@ -251,6 +251,7 @@ func (a APIDefinitionLoader) MakeSpec(def *apidef.APIDefinition, logger *logrus. // Create and init the virtual Machine if config.Global().EnableJSVM { + spec.JSVM = InitJSVM() spec.JSVM.Init(spec, logger) } @@ -792,7 +793,7 @@ func (a APIDefinitionLoader) compileVirtualPathspathSpec(paths []apidef.VirtualM // Extend with method actions newSpec.VirtualPathSpec = stringSpec - preLoadVirtualMetaCode(&newSpec.VirtualPathSpec, &apiSpec.JSVM) + preLoadVirtualMetaCode(&newSpec.VirtualPathSpec, apiSpec.JSVM) urlSpec = append(urlSpec, newSpec) } diff --git a/config/config.go b/config/config.go index 3743d6e7940..5f9a479a138 100644 --- a/config/config.go +++ b/config/config.go @@ -281,6 +281,7 @@ type Config struct { ControlAPIPort int `json:"control_api_port"` EnableCustomDomains bool `json:"enable_custom_domains"` EnableJSVM bool `json:"enable_jsvm"` + JSVM string `json:"jsvm"` JSVMTimeout int `json:"jsvm_timeout"` CoProcessOptions CoProcessConfig `json:"coprocess_options"` HideGeneratorHeader bool `json:"hide_generator_header"` diff --git a/jsvm_event_handler.go b/jsvm_event_handler.go index 53015832963..c9a0371a03c 100644 --- a/jsvm_event_handler.go +++ b/jsvm_event_handler.go @@ -50,5 +50,5 @@ func (l *JSVMEventHandler) HandleEvent(em config.EventMessage) { } // Execute the method name with the JSON object - GlobalEventsJSVM.VM.Run(l.methodName + `.DoProcessEvent(` + string(msgAsJSON) + `,` + l.SpecJSON + `);`) + GlobalEventsJSVM.Run(l.methodName + `.DoProcessEvent(` + string(msgAsJSON) + `,` + l.SpecJSON + `);`) } diff --git a/jsvm_goja.go b/jsvm_goja.go new file mode 100644 index 00000000000..6a6cab95925 --- /dev/null +++ b/jsvm_goja.go @@ -0,0 +1,32 @@ +package main + +import ( + "time" + + "github.com/Sirupsen/logrus" + "github.com/TykTechnologies/tyk/apidef" + "github.com/TykTechnologies/tyk/config" + _ "github.com/robertkrimen/otto/underscore" +) + +type TykJSVM interface { + Init(spec *APISpec, logger *logrus.Entry) + LoadJSPaths(paths []string, prefix string) + LoadTykJSApi() + RunJSRequestDynamic(d *DynamicMiddleware, logger *logrus.Entry, requestAsJson string, sessionAsJson string, specAsJson string) (error, int, string) + RunJSRequestVirtual(d *VirtualEndpoint, logger *logrus.Entry, vmeta *apidef.VirtualMeta, requestAsJson string, sessionAsJson string, specAsJson string) (error, int, string) + Run(s string) (interface{}, error) + GetLog() *logrus.Entry + GetRawLog() *logrus.Logger + GetTimeout() time.Duration +} + +func InitJSVM() TykJSVM { + jsvm := &OttoJSVM{} + + switch config.Global().JSVM { + case "goja": + default: + } + return jsvm +} diff --git a/jsvm_otto.go b/jsvm_otto.go new file mode 100644 index 00000000000..601bb72d530 --- /dev/null +++ b/jsvm_otto.go @@ -0,0 +1,519 @@ +package main + +import ( + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/TykTechnologies/tyk/apidef" + "github.com/TykTechnologies/tyk/config" + "github.com/TykTechnologies/tyk/user" + "github.com/robertkrimen/otto" + _ "github.com/robertkrimen/otto/underscore" + + "github.com/Sirupsen/logrus" +) + +// --- Utility functions during startup to ensure a sane VM is present for each API Def ---- + +type OttoJSVM struct { + Spec *APISpec + VM *otto.Otto + Timeout time.Duration + Log *logrus.Entry // logger used by the JS code + RawLog *logrus.Logger // logger used by `rawlog` func to avoid formatting +} + +const defaultJSVMTimeout = 5 + +func (j *OttoJSVM) GetLog() *logrus.Entry { + return j.Log +} + +func (j *OttoJSVM) GetRawLog() *logrus.Logger { + return j.RawLog +} + +func (j *OttoJSVM) GetTimeout() time.Duration { + return j.Timeout +} + +// Init creates the JSVM with the core library and sets up a default +// timeout. +func (j *OttoJSVM) Init(spec *APISpec, logger *logrus.Entry) { + vm := otto.New() + logger = logger.WithField("prefix", "jsvm") + + // Init TykJS namespace, constructors etc. + if _, err := vm.Run(coreJS); err != nil { + logger.WithError(err).Error("Could not load TykJS") + return + } + + // Load user's TykJS on top, if any + if path := config.Global().TykJSPath; path != "" { + f, err := os.Open(path) + if err == nil { + _, err = vm.Run(f) + f.Close() + + if err != nil { + logger.WithError(err).Error("Could not load user's TykJS") + } + } + } + + j.VM = vm + j.Spec = spec + + // Add environment API + j.LoadTykJSApi() + + if jsvmTimeout := config.Global().JSVMTimeout; jsvmTimeout <= 0 { + j.Timeout = time.Duration(defaultJSVMTimeout) * time.Second + logger.Debugf("Default JSVM timeout used: %v", j.Timeout) + } else { + j.Timeout = time.Duration(jsvmTimeout) * time.Second + logger.Debugf("Custom JSVM timeout: %v", j.Timeout) + } + + j.Log = logger // use the global logger by default + j.RawLog = rawLog +} + +// LoadJSPaths will load JS classes and functionality in to the VM by file +func (j *OttoJSVM) LoadJSPaths(paths []string, prefix string) { + for _, mwPath := range paths { + if prefix != "" { + mwPath = filepath.Join(prefix, mwPath) + } + j.Log.Info("Loading JS File: ", mwPath) + f, err := os.Open(mwPath) + if err != nil { + j.Log.WithError(err).Error("Failed to open JS middleware file") + continue + } + if _, err := j.VM.Run(f); err != nil { + j.Log.WithError(err).Error("Failed to load JS middleware") + } + f.Close() + } +} + +func (j *OttoJSVM) RunJSRequestDynamic(d *DynamicMiddleware, logger *logrus.Entry, requestAsJson string, sessionAsJson string, specAsJson string) (error, int, string) { + middlewareClassname := d.MiddlewareClassName + vm := j.VM.Copy() + vm.Interrupt = make(chan func(), 1) + logger.Debug("Running: ", middlewareClassname) + // buffered, leaving no chance of a goroutine leak since the + // spawned goroutine will send 0 or 1 values. + ret := make(chan otto.Value, 1) + errRet := make(chan error, 1) + go func() { + defer func() { + // the VM executes the panic func that gets it + // to stop, so we must recover here to not crash + // the whole Go program. + recover() + }() + returnRaw, err := vm.Run(middlewareClassname + `.DoProcessRequest(` + string(requestAsJson) + `, ` + string(sessionAsJson) + `, ` + specAsJson + `);`) + ret <- returnRaw + errRet <- err + }() + var returnRaw otto.Value + t := time.NewTimer(d.Spec.JSVM.GetTimeout()) + select { + case returnRaw = <-ret: + if err := <-errRet; err != nil { + logger.WithError(err).Error("Failed to run JS middleware") + return nil, http.StatusOK, "" + } + t.Stop() + case <-t.C: + t.Stop() + logger.Error("JS middleware timed out after ", d.Spec.JSVM.GetTimeout()) + vm.Interrupt <- func() { + // only way to stop the VM is to send it a func + // that panics. + panic("stop") + } + return nil, http.StatusOK, "" + } + returnDataStr, _ := returnRaw.ToString() + return nil, -1, returnDataStr +} + +func (j *OttoJSVM) RunJSRequestVirtual(d *VirtualEndpoint, logger *logrus.Entry, vmeta *apidef.VirtualMeta, requestAsJson string, sessionAsJson string, specAsJson string) (error, int, string) { + vm := j.VM.Copy() + fmt.Println("VIRTUALREQUEST") + vm.Interrupt = make(chan func(), 1) + d.Logger().Debug("Running: ", vmeta.ResponseFunctionName) + // buffered, leaving no chance of a goroutine leak since the + // spawned goroutine will send 0 or 1 values. + ret := make(chan otto.Value, 1) + errRet := make(chan error, 1) + go func() { + defer func() { + // the VM executes the panic func that gets it + // to stop, so we must recover here to not crash + // the whole Go program. + recover() + }() + returnRaw, err := j.Run(vmeta.ResponseFunctionName + `(` + requestAsJson + `, ` + sessionAsJson + `, ` + specAsJson + `);`) + ret <- returnRaw.(otto.Value) + errRet <- err + }() + var returnRaw otto.Value + t := time.NewTimer(j.GetTimeout()) + select { + case returnRaw = <-ret: + if err := <-errRet; err != nil { + d.Logger().WithError(err).Error("Failed to run JS middleware") + return nil, -1, "" + } + t.Stop() + case <-t.C: + t.Stop() + d.Logger().Error("JS middleware timed out after ", j.GetTimeout()) + vm.Interrupt <- func() { + // only way to stop the VM is to send it a func + // that panics. + panic("stop") + } + return nil, -1, "" + } + returnDataStr, _ := returnRaw.ToString() + return nil, -1, returnDataStr +} + +type TykJSHttpRequest struct { + Method string + Body string + Headers map[string]string + Domain string + Resource string + FormData map[string]string +} + +type TykJSHttpResponse struct { + Code int + Body string + Headers map[string][]string + + // Make this compatible with BatchReplyUnit + CodeComp int `json:"code"` + BodyComp string `json:"body"` + HeadersComp map[string][]string `json:"headers"` +} + +func (j *OttoJSVM) LoadTykJSApi() { + // Enable a log + j.VM.Set("log", func(call otto.FunctionCall) otto.Value { + j.Log.WithFields(logrus.Fields{ + "type": "log-msg", + }).Info(call.Argument(0).String()) + return otto.Value{} + }) + j.VM.Set("rawlog", func(call otto.FunctionCall) otto.Value { + j.RawLog.Print(call.Argument(0).String() + "\n") + return otto.Value{} + }) + + // these two needed for non-utf8 bodies + j.VM.Set("b64dec", func(call otto.FunctionCall) otto.Value { + in := call.Argument(0).String() + out, err := base64.StdEncoding.DecodeString(in) + + // Fallback to RawStdEncoding: + if err != nil { + out, err = base64.RawStdEncoding.DecodeString(in) + if err != nil { + j.Log.WithError(err).Error("Failed to base64 decode") + return otto.Value{} + } + } + returnVal, err := j.VM.ToValue(string(out)) + if err != nil { + j.Log.WithError(err).Error("Failed to base64 decode") + return otto.Value{} + } + return returnVal + }) + j.VM.Set("b64enc", func(call otto.FunctionCall) otto.Value { + in := []byte(call.Argument(0).String()) + out := base64.StdEncoding.EncodeToString(in) + returnVal, err := j.VM.ToValue(out) + if err != nil { + j.Log.WithError(err).Error("Failed to base64 encode") + return otto.Value{} + } + return returnVal + }) + + j.VM.Set("rawb64dec", func(call otto.FunctionCall) otto.Value { + in := call.Argument(0).String() + out, err := base64.RawStdEncoding.DecodeString(in) + if err != nil { + if err != nil { + j.Log.WithError(err).Error("Failed to base64 decode") + return otto.Value{} + } + } + returnVal, err := j.VM.ToValue(string(out)) + if err != nil { + j.Log.WithError(err).Error("Failed to base64 decode") + return otto.Value{} + } + return returnVal + }) + j.VM.Set("rawb64enc", func(call otto.FunctionCall) otto.Value { + in := []byte(call.Argument(0).String()) + out := base64.RawStdEncoding.EncodeToString(in) + returnVal, err := j.VM.ToValue(out) + if err != nil { + j.Log.WithError(err).Error("Failed to base64 encode") + return otto.Value{} + } + return returnVal + }) + + // Enable the creation of HTTP Requsts + j.VM.Set("TykMakeHttpRequest", func(call otto.FunctionCall) otto.Value { + jsonHRO := call.Argument(0).String() + if jsonHRO == "undefined" { + // Nope, return nothing + return otto.Value{} + } + hro := TykJSHttpRequest{} + if err := json.Unmarshal([]byte(jsonHRO), &hro); err != nil { + j.Log.WithError(err).Error("JSVM: Failed to deserialise HTTP Request object") + return otto.Value{} + } + + // Make the request + domain := hro.Domain + data := url.Values{} + for k, v := range hro.FormData { + data.Set(k, v) + } + + u, _ := url.ParseRequestURI(domain) + u.Path = hro.Resource + urlStr := u.String() // "https://api.com/user/" + + var d string + if hro.Body != "" { + d = hro.Body + } else if len(hro.FormData) > 0 { + d = data.Encode() + } + + r, _ := http.NewRequest(hro.Method, urlStr, nil) + + if d != "" { + r, _ = http.NewRequest(hro.Method, urlStr, strings.NewReader(d)) + } + + for k, v := range hro.Headers { + r.Header.Set(k, v) + } + r.Close = true + + tr := &http.Transport{TLSClientConfig: &tls.Config{}} + if cert := getUpstreamCertificate(r.Host, j.Spec); cert != nil { + tr.TLSClientConfig.Certificates = []tls.Certificate{*cert} + } + + if config.Global().ProxySSLInsecureSkipVerify { + tr.TLSClientConfig.InsecureSkipVerify = true + } + + if j.Spec.Proxy.Transport.SSLInsecureSkipVerify { + tr.TLSClientConfig.InsecureSkipVerify = true + } + + tr.DialTLS = dialTLSPinnedCheck(j.Spec, tr.TLSClientConfig) + + tr.Proxy = proxyFromAPI(j.Spec) + + // using new Client each time should be ok, since we closing connection every time + client := &http.Client{Transport: tr} + resp, err := client.Do(r) + if err != nil { + j.Log.WithError(err).Error("Request failed") + return otto.Value{} + } + + body, _ := ioutil.ReadAll(resp.Body) + bodyStr := string(body) + tykResp := TykJSHttpResponse{ + Code: resp.StatusCode, + Body: bodyStr, + Headers: resp.Header, + CodeComp: resp.StatusCode, + BodyComp: bodyStr, + HeadersComp: resp.Header, + } + + retAsStr, _ := json.Marshal(tykResp) + returnVal, err := j.VM.ToValue(string(retAsStr)) + if err != nil { + j.Log.WithError(err).Error("Failed to encode return value") + return otto.Value{} + } + + return returnVal + }) + + // Expose Setters and Getters in the REST API for a key: + j.VM.Set("TykGetKeyData", func(call otto.FunctionCall) otto.Value { + apiKey := call.Argument(0).String() + apiId := call.Argument(1).String() + + obj, _ := handleGetDetail(apiKey, apiId, false) + bs, _ := json.Marshal(obj) + + returnVal, err := j.VM.ToValue(string(bs)) + if err != nil { + j.Log.WithError(err).Error("Failed to encode return value") + return otto.Value{} + } + + return returnVal + }) + + j.VM.Set("TykSetKeyData", func(call otto.FunctionCall) otto.Value { + apiKey := call.Argument(0).String() + encoddedSession := call.Argument(1).String() + suppressReset := call.Argument(2).String() + + newSession := user.SessionState{} + err := json.Unmarshal([]byte(encoddedSession), &newSession) + if err != nil { + j.Log.WithError(err).Error("Failed to decode the sesison data") + return otto.Value{} + } + + doAddOrUpdate(apiKey, &newSession, suppressReset == "1") + + return otto.Value{} + }) + + // Batch request method + unsafeBatchHandler := BatchRequestHandler{} + j.VM.Set("TykBatchRequest", func(call otto.FunctionCall) otto.Value { + requestSet := call.Argument(0).String() + j.Log.Debug("Batch input is: ", requestSet) + + bs, err := unsafeBatchHandler.ManualBatchRequest([]byte(requestSet)) + if err != nil { + j.Log.WithError(err).Error("Batch request error") + return otto.Value{} + } + + returnVal, err := j.VM.ToValue(string(bs)) + if err != nil { + j.Log.WithError(err).Error("Failed to encode return value") + return otto.Value{} + } + + return returnVal + }) + + j.Run(`function TykJsResponse(response, session_meta) { + return JSON.stringify({Response: response, SessionMeta: session_meta}) + }`) +} + +func (j *OttoJSVM) Run(s string) (interface{}, error) { + return j.VM.Run(s) +} + +const coreJS = ` +var TykJS = { + TykMiddleware: { + MiddlewareComponentMeta: function(configuration) { + this.configuration = configuration + } + }, + TykEventHandlers: { + EventHandlerComponentMeta: function() {} + } +} + +TykJS.TykMiddleware.MiddlewareComponentMeta.prototype.ProcessRequest = function(request, session, config) { + log("Process Request Not Implemented") + return request +} + +TykJS.TykMiddleware.MiddlewareComponentMeta.prototype.DoProcessRequest = function(request, session, config) { + request.Body = b64dec(request.Body) + var processed_request = this.ProcessRequest(request, session, config) + + if (!processed_request) { + log("Middleware didn't return request object!") + return + } + + // Reset the headers object + processed_request.Request.Headers = {} + processed_request.Request.Body = b64enc(processed_request.Request.Body) + + return JSON.stringify(processed_request) +} + +// The user-level middleware component +TykJS.TykMiddleware.NewMiddleware = function(configuration) { + TykJS.TykMiddleware.MiddlewareComponentMeta.call(this, configuration) +} + +// Set up object inheritance +TykJS.TykMiddleware.NewMiddleware.prototype = Object.create(TykJS.TykMiddleware.MiddlewareComponentMeta.prototype) +TykJS.TykMiddleware.NewMiddleware.prototype.constructor = TykJS.TykMiddleware.NewMiddleware + +TykJS.TykMiddleware.NewMiddleware.prototype.NewProcessRequest = function(callback) { + this.ProcessRequest = callback +} + +TykJS.TykMiddleware.NewMiddleware.prototype.ReturnData = function(request, session) { + return {Request: request, SessionMeta: session} +} + +TykJS.TykMiddleware.NewMiddleware.prototype.ReturnAuthData = function(request, session) { + return {Request: request, Session: session} +} + +// Event Handler implementation + +TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype.DoProcessEvent = function(event, context) { + // call the handler + log("Calling built - in handle") + this.Handle(event, context) + return +} + +TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype.Handle = function(request, context) { + log("Handler not implemented!") + return request +} + +// The user-level event handler component +TykJS.TykEventHandlers.NewEventHandler = function() { + TykJS.TykEventHandlers.EventHandlerComponentMeta.call(this) +} + +// Set up object inheritance for events +TykJS.TykEventHandlers.NewEventHandler.prototype = Object.create(TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype) +TykJS.TykEventHandlers.NewEventHandler.prototype.constructor = TykJS.TykEventHandlers.NewEventHandler + +TykJS.TykEventHandlers.NewEventHandler.prototype.NewHandler = function(callback) { + this.Handle = callback +};` diff --git a/main.go b/main.go index 36eb72d2569..45bee4cd34a 100644 --- a/main.go +++ b/main.go @@ -60,7 +60,7 @@ var ( rawLog = logger.GetRaw() templates *template.Template analytics RedisAnalyticsHandler - GlobalEventsJSVM JSVM + GlobalEventsJSVM = InitJSVM() memProfFile *os.File MainNotifier RedisNotifier DefaultOrgStore DefaultSessionManager diff --git a/mw_js_plugin.go b/mw_js_plugin.go index a9cbdd4f74d..1bc590eea4f 100644 --- a/mw_js_plugin.go +++ b/mw_js_plugin.go @@ -2,26 +2,15 @@ package main import ( "bytes" - "crypto/tls" - "encoding/base64" "encoding/json" "errors" "io/ioutil" "net/http" "net/url" - "os" - "path/filepath" "reflect" - "strings" "time" - "github.com/robertkrimen/otto" - _ "github.com/robertkrimen/otto/underscore" - - "github.com/TykTechnologies/tyk/config" "github.com/TykTechnologies/tyk/user" - - "github.com/Sirupsen/logrus" ) // Lets the user override and return a response from middleware @@ -151,46 +140,14 @@ func (d *DynamicMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Reques } // Run the middleware - middlewareClassname := d.MiddlewareClassName - vm := d.Spec.JSVM.VM.Copy() - vm.Interrupt = make(chan func(), 1) - logger.Debug("Running: ", middlewareClassname) - // buffered, leaving no chance of a goroutine leak since the - // spawned goroutine will send 0 or 1 values. - ret := make(chan otto.Value, 1) - errRet := make(chan error, 1) - go func() { - defer func() { - // the VM executes the panic func that gets it - // to stop, so we must recover here to not crash - // the whole Go program. - recover() - }() - returnRaw, err := vm.Run(middlewareClassname + `.DoProcessRequest(` + string(requestAsJson) + `, ` + string(sessionAsJson) + `, ` + specAsJson + `);`) - ret <- returnRaw - errRet <- err - }() - var returnRaw otto.Value - t := time.NewTimer(d.Spec.JSVM.Timeout) - select { - case returnRaw = <-ret: - if err := <-errRet; err != nil { - logger.WithError(err).Error("Failed to run JS middleware") - return nil, http.StatusOK - } - t.Stop() - case <-t.C: - t.Stop() - logger.Error("JS middleware timed out after ", d.Spec.JSVM.Timeout) - vm.Interrupt <- func() { - // only way to stop the VM is to send it a func - // that panics. - panic("stop") - } - return nil, http.StatusOK - } - returnDataStr, _ := returnRaw.ToString() + err, code, returnDataStr := d.Spec.JSVM.RunJSRequestDynamic(d, logger, string(requestAsJson), string(specAsJson), string(sessionAsJson)) + if err != nil { + //TODO + } + if code != -1 { + return nil, code + } // Decode the return object newRequestData := VMReturnObject{} if err := json.Unmarshal([]byte(returnDataStr), &newRequestData); err != nil { @@ -284,396 +241,3 @@ func mapStrsToIfaces(m map[string]string) map[string]interface{} { } return m2 } - -// --- Utility functions during startup to ensure a sane VM is present for each API Def ---- - -type JSVM struct { - Spec *APISpec - VM *otto.Otto - Timeout time.Duration - Log *logrus.Entry // logger used by the JS code - RawLog *logrus.Logger // logger used by `rawlog` func to avoid formatting -} - -const defaultJSVMTimeout = 5 - -// Init creates the JSVM with the core library and sets up a default -// timeout. -func (j *JSVM) Init(spec *APISpec, logger *logrus.Entry) { - vm := otto.New() - logger = logger.WithField("prefix", "jsvm") - - // Init TykJS namespace, constructors etc. - if _, err := vm.Run(coreJS); err != nil { - logger.WithError(err).Error("Could not load TykJS") - return - } - - // Load user's TykJS on top, if any - if path := config.Global().TykJSPath; path != "" { - f, err := os.Open(path) - if err == nil { - _, err = vm.Run(f) - f.Close() - - if err != nil { - logger.WithError(err).Error("Could not load user's TykJS") - } - } - } - - j.VM = vm - j.Spec = spec - - // Add environment API - j.LoadTykJSApi() - - if jsvmTimeout := config.Global().JSVMTimeout; jsvmTimeout <= 0 { - j.Timeout = time.Duration(defaultJSVMTimeout) * time.Second - logger.Debugf("Default JSVM timeout used: %v", j.Timeout) - } else { - j.Timeout = time.Duration(jsvmTimeout) * time.Second - logger.Debugf("Custom JSVM timeout: %v", j.Timeout) - } - - j.Log = logger // use the global logger by default - j.RawLog = rawLog -} - -// LoadJSPaths will load JS classes and functionality in to the VM by file -func (j *JSVM) LoadJSPaths(paths []string, prefix string) { - for _, mwPath := range paths { - if prefix != "" { - mwPath = filepath.Join(prefix, mwPath) - } - j.Log.Info("Loading JS File: ", mwPath) - f, err := os.Open(mwPath) - if err != nil { - j.Log.WithError(err).Error("Failed to open JS middleware file") - continue - } - if _, err := j.VM.Run(f); err != nil { - j.Log.WithError(err).Error("Failed to load JS middleware") - } - f.Close() - } -} - -type TykJSHttpRequest struct { - Method string - Body string - Headers map[string]string - Domain string - Resource string - FormData map[string]string -} - -type TykJSHttpResponse struct { - Code int - Body string - Headers map[string][]string - - // Make this compatible with BatchReplyUnit - CodeComp int `json:"code"` - BodyComp string `json:"body"` - HeadersComp map[string][]string `json:"headers"` -} - -func (j *JSVM) LoadTykJSApi() { - // Enable a log - j.VM.Set("log", func(call otto.FunctionCall) otto.Value { - j.Log.WithFields(logrus.Fields{ - "type": "log-msg", - }).Info(call.Argument(0).String()) - return otto.Value{} - }) - j.VM.Set("rawlog", func(call otto.FunctionCall) otto.Value { - j.RawLog.Print(call.Argument(0).String() + "\n") - return otto.Value{} - }) - - // these two needed for non-utf8 bodies - j.VM.Set("b64dec", func(call otto.FunctionCall) otto.Value { - in := call.Argument(0).String() - out, err := base64.StdEncoding.DecodeString(in) - - // Fallback to RawStdEncoding: - if err != nil { - out, err = base64.RawStdEncoding.DecodeString(in) - if err != nil { - j.Log.WithError(err).Error("Failed to base64 decode") - return otto.Value{} - } - } - returnVal, err := j.VM.ToValue(string(out)) - if err != nil { - j.Log.WithError(err).Error("Failed to base64 decode") - return otto.Value{} - } - return returnVal - }) - j.VM.Set("b64enc", func(call otto.FunctionCall) otto.Value { - in := []byte(call.Argument(0).String()) - out := base64.StdEncoding.EncodeToString(in) - returnVal, err := j.VM.ToValue(out) - if err != nil { - j.Log.WithError(err).Error("Failed to base64 encode") - return otto.Value{} - } - return returnVal - }) - - j.VM.Set("rawb64dec", func(call otto.FunctionCall) otto.Value { - in := call.Argument(0).String() - out, err := base64.RawStdEncoding.DecodeString(in) - if err != nil { - if err != nil { - j.Log.WithError(err).Error("Failed to base64 decode") - return otto.Value{} - } - } - returnVal, err := j.VM.ToValue(string(out)) - if err != nil { - j.Log.WithError(err).Error("Failed to base64 decode") - return otto.Value{} - } - return returnVal - }) - j.VM.Set("rawb64enc", func(call otto.FunctionCall) otto.Value { - in := []byte(call.Argument(0).String()) - out := base64.RawStdEncoding.EncodeToString(in) - returnVal, err := j.VM.ToValue(out) - if err != nil { - j.Log.WithError(err).Error("Failed to base64 encode") - return otto.Value{} - } - return returnVal - }) - - // Enable the creation of HTTP Requsts - j.VM.Set("TykMakeHttpRequest", func(call otto.FunctionCall) otto.Value { - jsonHRO := call.Argument(0).String() - if jsonHRO == "undefined" { - // Nope, return nothing - return otto.Value{} - } - hro := TykJSHttpRequest{} - if err := json.Unmarshal([]byte(jsonHRO), &hro); err != nil { - j.Log.WithError(err).Error("JSVM: Failed to deserialise HTTP Request object") - return otto.Value{} - } - - // Make the request - domain := hro.Domain - data := url.Values{} - for k, v := range hro.FormData { - data.Set(k, v) - } - - u, _ := url.ParseRequestURI(domain + hro.Resource) - urlStr := u.String() // "https://api.com/user/" - - var d string - if hro.Body != "" { - d = hro.Body - } else if len(hro.FormData) > 0 { - d = data.Encode() - } - - r, _ := http.NewRequest(hro.Method, urlStr, nil) - - if d != "" { - r, _ = http.NewRequest(hro.Method, urlStr, strings.NewReader(d)) - } - - for k, v := range hro.Headers { - r.Header.Set(k, v) - } - r.Close = true - - tr := &http.Transport{TLSClientConfig: &tls.Config{}} - if cert := getUpstreamCertificate(r.Host, j.Spec); cert != nil { - tr.TLSClientConfig.Certificates = []tls.Certificate{*cert} - } - - if config.Global().ProxySSLInsecureSkipVerify { - tr.TLSClientConfig.InsecureSkipVerify = true - } - - if j.Spec.Proxy.Transport.SSLInsecureSkipVerify { - tr.TLSClientConfig.InsecureSkipVerify = true - } - - tr.DialTLS = dialTLSPinnedCheck(j.Spec, tr.TLSClientConfig) - - tr.Proxy = proxyFromAPI(j.Spec) - - // using new Client each time should be ok, since we closing connection every time - client := &http.Client{Transport: tr} - resp, err := client.Do(r) - if err != nil { - j.Log.WithError(err).Error("Request failed") - return otto.Value{} - } - - body, _ := ioutil.ReadAll(resp.Body) - bodyStr := string(body) - tykResp := TykJSHttpResponse{ - Code: resp.StatusCode, - Body: bodyStr, - Headers: resp.Header, - CodeComp: resp.StatusCode, - BodyComp: bodyStr, - HeadersComp: resp.Header, - } - - retAsStr, _ := json.Marshal(tykResp) - returnVal, err := j.VM.ToValue(string(retAsStr)) - if err != nil { - j.Log.WithError(err).Error("Failed to encode return value") - return otto.Value{} - } - - return returnVal - }) - - // Expose Setters and Getters in the REST API for a key: - j.VM.Set("TykGetKeyData", func(call otto.FunctionCall) otto.Value { - apiKey := call.Argument(0).String() - apiId := call.Argument(1).String() - - obj, _ := handleGetDetail(apiKey, apiId, false) - bs, _ := json.Marshal(obj) - - returnVal, err := j.VM.ToValue(string(bs)) - if err != nil { - j.Log.WithError(err).Error("Failed to encode return value") - return otto.Value{} - } - - return returnVal - }) - - j.VM.Set("TykSetKeyData", func(call otto.FunctionCall) otto.Value { - apiKey := call.Argument(0).String() - encoddedSession := call.Argument(1).String() - suppressReset := call.Argument(2).String() - - newSession := user.SessionState{} - err := json.Unmarshal([]byte(encoddedSession), &newSession) - if err != nil { - j.Log.WithError(err).Error("Failed to decode the sesison data") - return otto.Value{} - } - - doAddOrUpdate(apiKey, &newSession, suppressReset == "1", false) - - return otto.Value{} - }) - - // Batch request method - unsafeBatchHandler := BatchRequestHandler{} - j.VM.Set("TykBatchRequest", func(call otto.FunctionCall) otto.Value { - requestSet := call.Argument(0).String() - j.Log.Debug("Batch input is: ", requestSet) - - bs, err := unsafeBatchHandler.ManualBatchRequest([]byte(requestSet)) - if err != nil { - j.Log.WithError(err).Error("Batch request error") - return otto.Value{} - } - - returnVal, err := j.VM.ToValue(string(bs)) - if err != nil { - j.Log.WithError(err).Error("Failed to encode return value") - return otto.Value{} - } - - return returnVal - }) - - j.VM.Run(`function TykJsResponse(response, session_meta) { - return JSON.stringify({Response: response, SessionMeta: session_meta}) - }`) -} - -const coreJS = ` -var TykJS = { - TykMiddleware: { - MiddlewareComponentMeta: function(configuration) { - this.configuration = configuration - } - }, - TykEventHandlers: { - EventHandlerComponentMeta: function() {} - } -} - -TykJS.TykMiddleware.MiddlewareComponentMeta.prototype.ProcessRequest = function(request, session, config) { - log("Process Request Not Implemented") - return request -} - -TykJS.TykMiddleware.MiddlewareComponentMeta.prototype.DoProcessRequest = function(request, session, config) { - request.Body = b64dec(request.Body) - var processed_request = this.ProcessRequest(request, session, config) - - if (!processed_request) { - log("Middleware didn't return request object!") - return - } - - // Reset the headers object - processed_request.Request.Headers = {} - processed_request.Request.Body = b64enc(processed_request.Request.Body) - - return JSON.stringify(processed_request) -} - -// The user-level middleware component -TykJS.TykMiddleware.NewMiddleware = function(configuration) { - TykJS.TykMiddleware.MiddlewareComponentMeta.call(this, configuration) -} - -// Set up object inheritance -TykJS.TykMiddleware.NewMiddleware.prototype = Object.create(TykJS.TykMiddleware.MiddlewareComponentMeta.prototype) -TykJS.TykMiddleware.NewMiddleware.prototype.constructor = TykJS.TykMiddleware.NewMiddleware - -TykJS.TykMiddleware.NewMiddleware.prototype.NewProcessRequest = function(callback) { - this.ProcessRequest = callback -} - -TykJS.TykMiddleware.NewMiddleware.prototype.ReturnData = function(request, session) { - return {Request: request, SessionMeta: session} -} - -TykJS.TykMiddleware.NewMiddleware.prototype.ReturnAuthData = function(request, session) { - return {Request: request, Session: session} -} - -// Event Handler implementation - -TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype.DoProcessEvent = function(event, context) { - // call the handler - log("Calling built - in handle") - this.Handle(event, context) - return -} - -TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype.Handle = function(request, context) { - log("Handler not implemented!") - return request -} - -// The user-level event handler component -TykJS.TykEventHandlers.NewEventHandler = function() { - TykJS.TykEventHandlers.EventHandlerComponentMeta.call(this) -} - -// Set up object inheritance for events -TykJS.TykEventHandlers.NewEventHandler.prototype = Object.create(TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype) -TykJS.TykEventHandlers.NewEventHandler.prototype.constructor = TykJS.TykEventHandlers.NewEventHandler - -TykJS.TykEventHandlers.NewEventHandler.prototype.NewHandler = function(callback) { - this.Handle = callback -};` diff --git a/mw_virtual_endpoint.go b/mw_virtual_endpoint.go index 4223f9c8f21..6eb3c03f4b5 100644 --- a/mw_virtual_endpoint.go +++ b/mw_virtual_endpoint.go @@ -5,17 +5,16 @@ import ( "encoding/base64" "encoding/json" "errors" + "fmt" "io" "io/ioutil" "net/http" "net/url" - "os" "reflect" "strconv" "strings" "time" - "github.com/robertkrimen/otto" _ "github.com/robertkrimen/otto/underscore" "github.com/TykTechnologies/tyk/apidef" @@ -55,37 +54,39 @@ func (d *VirtualEndpoint) Name() string { return "VirtualEndpoint" } -func preLoadVirtualMetaCode(meta *apidef.VirtualMeta, j *JSVM) { +func preLoadVirtualMetaCode(meta *apidef.VirtualMeta, j TykJSVM) { // the only call site uses (&foo, &bar) so meta and j won't be // nil. - var src interface{} + var src string + log := j.GetLog() + //to make this not panic for both jsvms always make sure it is read into a string switch meta.FunctionSourceType { case "file": - j.Log.Debug("Loading JS Endpoint File: ", meta.FunctionSourceURI) - f, err := os.Open(meta.FunctionSourceURI) + log.Debug("Loading JS Endpoint File: ", meta.FunctionSourceURI) + f, err := ioutil.ReadFile(meta.FunctionSourceURI) if err != nil { - j.Log.WithError(err).Error("Failed to open Endpoint JS") + log.WithError(err).Error("Failed to open Endpoint JS") return } - src = f + src = string(f) case "blob": if config.Global().DisableVirtualPathBlobs { - j.Log.Error("[JSVM] Blobs not allowed on this node") + log.Error("[JSVM] Blobs not allowed on this node") return } - j.Log.Debug("Loading JS blob") + log.Debug("Loading JS blob") js, err := base64.StdEncoding.DecodeString(meta.FunctionSourceURI) if err != nil { - j.Log.WithError(err).Error("Failed to load blob JS") + log.WithError(err).Error("Failed to load blob JS") return } - src = js + src = string(js) default: - j.Log.Error("Type must be either file or blob (base64)!") + log.Error("Type must be either file or blob (base64)!") return } - if _, err := j.VM.Run(src); err != nil { - j.Log.WithError(err).Error("Could not load virtual endpoint JS") + if _, err := j.Run(src); err != nil { + log.WithError(err).Error("Could not load virtual endpoint JS") } } @@ -174,47 +175,15 @@ func (d *VirtualEndpoint) ServeHTTPForCache(w http.ResponseWriter, r *http.Reque d.Logger().WithError(err).Error("Failed to encode session for VM") return nil } - // Run the middleware - vm := d.Spec.JSVM.VM.Copy() - vm.Interrupt = make(chan func(), 1) - d.Logger().Debug("Running: ", vmeta.ResponseFunctionName) - // buffered, leaving no chance of a goroutine leak since the - // spawned goroutine will send 0 or 1 values. - ret := make(chan otto.Value, 1) - errRet := make(chan error, 1) - go func() { - defer func() { - // the VM executes the panic func that gets it - // to stop, so we must recover here to not crash - // the whole Go program. - recover() - }() - returnRaw, err := vm.Run(vmeta.ResponseFunctionName + `(` + string(requestAsJson) + `, ` + string(sessionAsJson) + `, ` + specAsJson + `);`) - ret <- returnRaw - errRet <- err - }() - var returnRaw otto.Value - t := time.NewTimer(d.Spec.JSVM.Timeout) - select { - case returnRaw = <-ret: - if err := <-errRet; err != nil { - d.Logger().WithError(err).Error("Failed to run JS middleware") - return nil - } - t.Stop() - case <-t.C: - t.Stop() - d.Logger().Error("JS middleware timed out after ", d.Spec.JSVM.Timeout) - vm.Interrupt <- func() { - // only way to stop the VM is to send it a func - // that panics. - panic("stop") - } + err, code, returnDataStr := d.Spec.JSVM.RunJSRequestVirtual(d, d.logger, vmeta, string(requestAsJson), string(sessionAsJson), specAsJson) + if err != nil { + //TODO + fmt.Println(err) + } + if code != -1 { return nil } - returnDataStr, _ := returnRaw.ToString() - // Decode the return object newResponseData := VMResponseObject{} if err := json.Unmarshal([]byte(returnDataStr), &newResponseData); err != nil { diff --git a/mw_virtual_endpoint_test.go b/mw_virtual_endpoint_test.go index cb4ebcfb25a..d8f017a1b72 100644 --- a/mw_virtual_endpoint_test.go +++ b/mw_virtual_endpoint_test.go @@ -11,6 +11,7 @@ import ( const virtTestJS = ` function testVirtData(request, session, config) { + var resp = { Body: "foobar", Headers: { @@ -19,6 +20,7 @@ function testVirtData(request, session, config) { }, Code: 202 } + console.log(TykJsResponse(resp, session.meta_data)) return TykJsResponse(resp, session.meta_data) } ` From e087ef209e8814ef2724eab03b8ca90df51805d0 Mon Sep 17 00:00:00 2001 From: joshblakeley Date: Fri, 2 Nov 2018 15:47:28 +0000 Subject: [PATCH 2/8] add goja functions - needs testing --- jsvm_goja.go | 403 ++++++++++++++++++++++++++++++++++++++++++++++++++- jsvm_otto.go | 2 - 2 files changed, 401 insertions(+), 4 deletions(-) diff --git a/jsvm_goja.go b/jsvm_goja.go index 6a6cab95925..2b536060456 100644 --- a/jsvm_goja.go +++ b/jsvm_goja.go @@ -1,11 +1,21 @@ package main import ( + "crypto/tls" + "encoding/base64" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "path/filepath" + "strings" "time" "github.com/Sirupsen/logrus" "github.com/TykTechnologies/tyk/apidef" "github.com/TykTechnologies/tyk/config" + "github.com/TykTechnologies/tyk/user" + "github.com/dop251/goja" _ "github.com/robertkrimen/otto/underscore" ) @@ -22,11 +32,400 @@ type TykJSVM interface { } func InitJSVM() TykJSVM { - jsvm := &OttoJSVM{} switch config.Global().JSVM { case "goja": + return &GojaJSVM{} default: + return &OttoJSVM{} } - return jsvm +} + +type GojaJSVM struct { + Spec *APISpec + VM *goja.Runtime + Timeout time.Duration + Log *logrus.Entry // logger used by the JS code + RawLog *logrus.Logger // logger used by `rawlog` func to avoid formatting +} + +func (j *GojaJSVM) GetLog() *logrus.Entry { + return j.Log +} + +func (j *GojaJSVM) GetRawLog() *logrus.Logger { + return j.RawLog +} + +func (j *GojaJSVM) GetTimeout() time.Duration { + return j.Timeout +} + +// Init creates the JSVM with the core library and sets up a default +// timeout. +func (j *GojaJSVM) Init(spec *APISpec, logger *logrus.Entry) { + vm := goja.New() + logger = logger.WithField("prefix", "jsvm") + + // Init TykJS namespace, constructors etc. + if _, err := vm.RunString(coreJS); err != nil { + logger.WithError(err).Error("Could not load TykJS") + return + } + + // Load user's TykJS on top, if any + if path := config.Global().TykJSPath; path != "" { + f, err := ioutil.ReadFile(path) + if err == nil { + _, err = vm.RunString(string(f)) + + if err != nil { + logger.WithError(err).Error("Could not load user's TykJS") + } + } + } + + j.VM = vm + j.Spec = spec + + // Add environment API + j.LoadTykJSApi() + + if jsvmTimeout := config.Global().JSVMTimeout; jsvmTimeout <= 0 { + j.Timeout = time.Duration(defaultJSVMTimeout) * time.Second + logger.Debugf("Default JSVM timeout used: %v", j.Timeout) + } else { + j.Timeout = time.Duration(jsvmTimeout) * time.Second + logger.Debugf("Custom JSVM timeout: %v", j.Timeout) + } + + j.Log = logger // use the global logger by default + j.RawLog = rawLog +} + +// LoadJSPaths will load JS classes and functionality in to the VM by file +func (j *GojaJSVM) LoadJSPaths(paths []string, prefix string) { + for _, mwPath := range paths { + if prefix != "" { + mwPath = filepath.Join(prefix, mwPath) + } + j.Log.Info("Loading JS File: ", mwPath) + f, err := ioutil.ReadFile(mwPath) + if err != nil { + j.Log.WithError(err).Error("Failed to open JS middleware file") + continue + } + if _, err := j.VM.RunString(string(f)); err != nil { + j.Log.WithError(err).Error("Failed to load JS middleware") + } + } +} + +func (j *GojaJSVM) RunJSRequestDynamic(d *DynamicMiddleware, logger *logrus.Entry, requestAsJson string, sessionAsJson string, specAsJson string) (error, int, string) { + middlewareClassname := d.MiddlewareClassName + vm := j.VM + interrupt := make(chan func(), 1) + logger.Debug("Running: ", middlewareClassname) + // buffered, leaving no chance of a goroutine leak since the + // spawned goroutine will send 0 or 1 values. + ret := make(chan goja.Value, 1) + errRet := make(chan error, 1) + go func() { + defer func() { + // the VM executes the panic func that gets it + // to stop, so we must recover here to not crash + // the whole Go program. + recover() + }() + returnRaw, err := vm.RunString(middlewareClassname + `.DoProcessRequest(` + string(requestAsJson) + `, ` + string(sessionAsJson) + `, ` + specAsJson + `);`) + ret <- returnRaw + errRet <- err + }() + var returnRaw goja.Value + t := time.NewTimer(d.Spec.JSVM.GetTimeout()) + select { + case returnRaw = <-ret: + if err := <-errRet; err != nil { + logger.WithError(err).Error("Failed to run JS middleware") + return nil, http.StatusOK, "" + } + t.Stop() + case <-t.C: + t.Stop() + logger.Error("JS middleware timed out after ", d.Spec.JSVM.GetTimeout()) + interrupt <- func() { + // only way to stop the VM is to send it a func + // that panics. + panic("stop") + } + return nil, http.StatusOK, "" + } + returnDataStr := returnRaw.String() + return nil, -1, returnDataStr +} + +func (j *GojaJSVM) RunJSRequestVirtual(d *VirtualEndpoint, logger *logrus.Entry, vmeta *apidef.VirtualMeta, requestAsJson string, sessionAsJson string, specAsJson string) (error, int, string) { + + interrupt := make(chan func(), 1) + d.Logger().Debug("Running: ", vmeta.ResponseFunctionName) + // buffered, leaving no chance of a goroutine leak since the + // spawned goroutine will send 0 or 1 values. + ret := make(chan goja.Value, 1) + errRet := make(chan error, 1) + go func() { + defer func() { + // the VM executes the panic func that gets it + // to stop, so we must recover here to not crash + // the whole Go program. + recover() + }() + returnRaw, err := j.Run(vmeta.ResponseFunctionName + `(` + requestAsJson + `, ` + sessionAsJson + `, ` + specAsJson + `);`) + ret <- returnRaw.(goja.Value) + errRet <- err + }() + var returnRaw goja.Value + t := time.NewTimer(j.GetTimeout()) + select { + case returnRaw = <-ret: + if err := <-errRet; err != nil { + d.Logger().WithError(err).Error("Failed to run JS middleware") + return nil, -1, "" + } + t.Stop() + case <-t.C: + t.Stop() + d.Logger().Error("JS middleware timed out after ", j.GetTimeout()) + interrupt <- func() { + // only way to stop the VM is to send it a func + // that panics. + panic("stop") + } + return nil, -1, "" + } + returnDataStr := returnRaw.String() + return nil, -1, returnDataStr +} + +func (j *GojaJSVM) LoadTykJSApi() { + // Enable a log + j.VM.Set("log", func(call goja.FunctionCall) goja.Value { + j.Log.WithFields(logrus.Fields{ + "type": "log-msg", + }).Info(call.Argument(0).String()) + return nil + }) + j.VM.Set("rawlog", func(call goja.FunctionCall) goja.Value { + j.RawLog.Print(call.Argument(0).String() + "\n") + return nil + }) + + // these two needed for non-utf8 bodies + j.VM.Set("b64dec", func(call goja.FunctionCall) goja.Value { + in := call.Argument(0).String() + out, err := base64.StdEncoding.DecodeString(in) + + // Fallback to RawStdEncoding: + if err != nil { + out, err = base64.RawStdEncoding.DecodeString(in) + if err != nil { + j.Log.WithError(err).Error("Failed to base64 decode") + return nil + } + } + returnVal := j.VM.ToValue(string(out)) + if returnVal == nil { + j.Log.Error("Failed to base64 decode") + return nil + } + return returnVal + }) + j.VM.Set("b64enc", func(call goja.FunctionCall) goja.Value { + in := []byte(call.Argument(0).String()) + out := base64.StdEncoding.EncodeToString(in) + returnVal := j.VM.ToValue(out) + if returnVal == nil { + j.Log.Error("Failed to base64 encode") + return nil + } + return returnVal + }) + + j.VM.Set("rawb64dec", func(call goja.FunctionCall) goja.Value { + in := call.Argument(0).String() + out, err := base64.RawStdEncoding.DecodeString(in) + if err != nil { + if err != nil { + j.Log.WithError(err).Error("Failed to base64 decode") + return nil + } + } + returnVal := j.VM.ToValue(string(out)) + if returnVal == nil { + j.Log.WithError(err).Error("Failed to base64 decode") + return nil + } + return returnVal + }) + j.VM.Set("rawb64enc", func(call goja.FunctionCall) goja.Value { + in := []byte(call.Argument(0).String()) + out := base64.RawStdEncoding.EncodeToString(in) + returnVal := j.VM.ToValue(out) + if returnVal == nil { + j.Log.Error("Failed to base64 encode") + return nil + } + return returnVal + }) + + // Enable the creation of HTTP Requsts + j.VM.Set("TykMakeHttpRequest", func(call goja.FunctionCall) goja.Value { + jsonHRO := call.Argument(0).String() + if jsonHRO == "undefined" { + // Nope, return nothing + return nil + } + hro := TykJSHttpRequest{} + if err := json.Unmarshal([]byte(jsonHRO), &hro); err != nil { + j.Log.WithError(err).Error("JSVM: Failed to deserialise HTTP Request object") + return nil + } + + // Make the request + domain := hro.Domain + data := url.Values{} + for k, v := range hro.FormData { + data.Set(k, v) + } + + u, _ := url.ParseRequestURI(domain) + u.Path = hro.Resource + urlStr := u.String() // "https://api.com/user/" + + var d string + if hro.Body != "" { + d = hro.Body + } else if len(hro.FormData) > 0 { + d = data.Encode() + } + + r, _ := http.NewRequest(hro.Method, urlStr, nil) + + if d != "" { + r, _ = http.NewRequest(hro.Method, urlStr, strings.NewReader(d)) + } + + for k, v := range hro.Headers { + r.Header.Set(k, v) + } + r.Close = true + + tr := &http.Transport{TLSClientConfig: &tls.Config{}} + if cert := getUpstreamCertificate(r.Host, j.Spec); cert != nil { + tr.TLSClientConfig.Certificates = []tls.Certificate{*cert} + } + + if config.Global().ProxySSLInsecureSkipVerify { + tr.TLSClientConfig.InsecureSkipVerify = true + } + + if j.Spec.Proxy.Transport.SSLInsecureSkipVerify { + tr.TLSClientConfig.InsecureSkipVerify = true + } + + tr.DialTLS = dialTLSPinnedCheck(j.Spec, tr.TLSClientConfig) + + tr.Proxy = proxyFromAPI(j.Spec) + + // using new Client each time should be ok, since we closing connection every time + client := &http.Client{Transport: tr} + resp, err := client.Do(r) + if err != nil { + j.Log.WithError(err).Error("Request failed") + return nil + } + + body, _ := ioutil.ReadAll(resp.Body) + bodyStr := string(body) + tykResp := TykJSHttpResponse{ + Code: resp.StatusCode, + Body: bodyStr, + Headers: resp.Header, + CodeComp: resp.StatusCode, + BodyComp: bodyStr, + HeadersComp: resp.Header, + } + + retAsStr, _ := json.Marshal(tykResp) + returnVal := j.VM.ToValue(string(retAsStr)) + if returnVal == nil { + j.Log.WithError(err).Error("Failed to encode return value") + return nil + } + + return returnVal + }) + + // Expose Setters and Getters in the REST API for a key: + j.VM.Set("TykGetKeyData", func(call goja.FunctionCall) goja.Value { + apiKey := call.Argument(0).String() + apiId := call.Argument(1).String() + + obj, _ := handleGetDetail(apiKey, apiId, false) + bs, _ := json.Marshal(obj) + + returnVal := j.VM.ToValue(string(bs)) + if returnVal == nil { + j.Log.Error("Failed to encode return value") + return nil + } + + return returnVal + }) + + j.VM.Set("TykSetKeyData", func(call goja.FunctionCall) goja.Value { + apiKey := call.Argument(0).String() + encoddedSession := call.Argument(1).String() + suppressReset := call.Argument(2).String() + + newSession := user.SessionState{} + err := json.Unmarshal([]byte(encoddedSession), &newSession) + if err != nil { + j.Log.WithError(err).Error("Failed to decode the session data") + return nil + } + + doAddOrUpdate(apiKey, &newSession, suppressReset == "1") + + return nil + }) + + // Batch request method + unsafeBatchHandler := BatchRequestHandler{} + j.VM.Set("TykBatchRequest", func(call goja.FunctionCall) goja.Value { + requestSet := call.Argument(0).String() + j.Log.Debug("Batch input is: ", requestSet) + + bs, err := unsafeBatchHandler.ManualBatchRequest([]byte(requestSet)) + if err != nil { + j.Log.WithError(err).Error("Batch request error") + return nil + } + + returnVal := j.VM.ToValue(string(bs)) + if err != nil { + j.Log.WithError(err).Error("Failed to encode return value") + return nil + } + + return returnVal + }) + + j.Run(`function TykJsResponse(response, session_meta) { + return JSON.stringify({Response: response, SessionMeta: session_meta}) + }`) +} + +func (j *GojaJSVM) Run(s string) (interface{}, error) { + + return j.VM.RunString(s) } diff --git a/jsvm_otto.go b/jsvm_otto.go index 601bb72d530..8a684285dcd 100644 --- a/jsvm_otto.go +++ b/jsvm_otto.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "encoding/base64" "encoding/json" - "fmt" "io/ioutil" "net/http" "net/url" @@ -153,7 +152,6 @@ func (j *OttoJSVM) RunJSRequestDynamic(d *DynamicMiddleware, logger *logrus.Entr func (j *OttoJSVM) RunJSRequestVirtual(d *VirtualEndpoint, logger *logrus.Entry, vmeta *apidef.VirtualMeta, requestAsJson string, sessionAsJson string, specAsJson string) (error, int, string) { vm := j.VM.Copy() - fmt.Println("VIRTUALREQUEST") vm.Interrupt = make(chan func(), 1) d.Logger().Debug("Running: ", vmeta.ResponseFunctionName) // buffered, leaving no chance of a goroutine leak since the From a120df1df3e3d58e36277d1141918d437f184ec8 Mon Sep 17 00:00:00 2001 From: joshblakeley Date: Fri, 2 Nov 2018 16:30:08 +0000 Subject: [PATCH 3/8] add goja test --- jsvm_goja.go | 2 +- mw_virtual_endpoint_test.go | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/jsvm_goja.go b/jsvm_goja.go index 2b536060456..be8fa2ad72f 100644 --- a/jsvm_goja.go +++ b/jsvm_goja.go @@ -420,7 +420,7 @@ func (j *GojaJSVM) LoadTykJSApi() { return returnVal }) - j.Run(`function TykJsResponse(response, session_meta) { + j.VM.RunString(`function TykJsResponse(response, session_meta) { return JSON.stringify({Response: response, SessionMeta: session_meta}) }`) } diff --git a/mw_virtual_endpoint_test.go b/mw_virtual_endpoint_test.go index d8f017a1b72..44ec0a975bf 100644 --- a/mw_virtual_endpoint_test.go +++ b/mw_virtual_endpoint_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/TykTechnologies/tyk/apidef" + "github.com/TykTechnologies/tyk/config" "github.com/TykTechnologies/tyk/test" ) @@ -20,7 +21,7 @@ function testVirtData(request, session, config) { }, Code: 202 } - console.log(TykJsResponse(resp, session.meta_data)) + return TykJsResponse(resp, session.meta_data) } ` @@ -59,7 +60,28 @@ func testPrepareVirtualEndpoint(js string, method string, path string, proxyOnEr }) } -func TestVirtualEndpoint(t *testing.T) { +func TestVirtualEndpointOtto(t *testing.T) { + ts := newTykTestServer() + defer ts.Close() + + testPrepareVirtualEndpoint(virtTestJS, "GET") + + ts.Run(t, test.TestCase{ + Path: "/virt", + Code: 202, + BodyMatch: "foobar", + HeadersMatch: map[string]string{ + "data-foo": "x", + "data-bar-y": "3", + }, + }) +} + +func TestVirtualEndpointGoja(t *testing.T) { + globalConf := config.Global() + globalConf.JSVM = "goja" + config.SetGlobal(globalConf) + defer resetTestConfig() ts := newTykTestServer() defer ts.Close() From f64f909a9f26d5110524625bf753420fd8f17472 Mon Sep 17 00:00:00 2001 From: joshblakeley Date: Fri, 2 Nov 2018 16:42:23 +0000 Subject: [PATCH 4/8] plugin tests changed to accept new types --- mw_js_plugin_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/mw_js_plugin_test.go b/mw_js_plugin_test.go index 6b0106cca4f..302dfb113f7 100644 --- a/mw_js_plugin_test.go +++ b/mw_js_plugin_test.go @@ -27,7 +27,7 @@ func TestJSVMLogs(t *testing.T) { log.Out = &buf log.Formatter = new(prefixed.TextFormatter) - jsvm := JSVM{} + jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) jsvm.RawLog = logrus.New() @@ -81,7 +81,8 @@ func TestJSVMBody(t *testing.T) { } body := "foô \uffff \u0000 \xff bàr" req := httptest.NewRequest("GET", "/foo", strings.NewReader(body)) - jsvm := JSVM{} + jsvm := &OttoJSVM{} + jsvm.Init(nil, logrus.NewEntry(log)) const js = ` @@ -117,7 +118,7 @@ func TestJSVMProcessTimeout(t *testing.T) { Pre: true, } req := httptest.NewRequest("GET", "/foo", strings.NewReader("body")) - jsvm := JSVM{} + jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) jsvm.Timeout = time.Millisecond @@ -165,7 +166,7 @@ testJSVMData.NewProcessRequest(function(request, session, spec) { MiddlewareClassName: "testJSVMData", Pre: true, } - jsvm := JSVM{} + jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) if _, err := jsvm.VM.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) @@ -201,7 +202,7 @@ testJSVMData.NewProcessRequest(function(request, session, config) { MiddlewareClassName: "testJSVMData", Pre: true, } - jsvm := JSVM{} + jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) if _, err := jsvm.VM.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) @@ -247,7 +248,7 @@ testJSVMData.NewProcessRequest(function(request, session, config) { MiddlewareClassName: "testJSVMData", Pre: true, } - jsvm := JSVM{} + jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) if _, err := jsvm.VM.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) @@ -296,7 +297,7 @@ testJSVMCore.NewProcessRequest(function(request, session, config) { globalConf.TykJSPath = old config.SetGlobal(globalConf) }() - jsvm := JSVM{} + jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) if _, err := jsvm.VM.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) @@ -463,7 +464,7 @@ func TestTykMakeHTTPRequest(t *testing.T) { } func TestJSVMBase64(t *testing.T) { - jsvm := JSVM{} + jsvm := OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) inputString := "teststring" From 44034cd92c60d2fa26b16cdbc3cc8c52d3ff9ae3 Mon Sep 17 00:00:00 2001 From: joshblakeley Date: Mon, 5 Nov 2018 14:12:13 +0000 Subject: [PATCH 5/8] js plugin tests for both drivers/ changes to logic for loading mw and coprocess changes to fix tests --- api_loader.go | 16 +- apidef/api_definitions.go | 1 + coprocess_bundle.go | 2 +- coprocess_test.go | 1 + jsvm_goja.go | 7 +- jsvm_otto.go | 85 +------ main.go | 12 +- mw_js_plugin.go | 86 ++++++- mw_js_plugin_test.go | 520 ++++++++++++++++++++++++++++++++++++-- 9 files changed, 606 insertions(+), 124 deletions(-) diff --git a/api_loader.go b/api_loader.go index 70c036dd3ad..03c078545be 100644 --- a/api_loader.go +++ b/api_loader.go @@ -212,7 +212,7 @@ func processSpec(spec *APISpec, apisByListen map[string]int, mwPaths, mwAuthCheckFunc, mwPreFuncs, mwPostFuncs, mwPostAuthCheckFuncs, mwDriver = loadCustomMiddleware(spec) - if config.Global().EnableJSVM && mwDriver == apidef.OttoDriver { + if config.Global().EnableJSVM && (mwDriver == apidef.OttoDriver || mwDriver == apidef.GojaDriver) { spec.JSVM.LoadJSPaths(mwPaths, prefix) } @@ -277,7 +277,7 @@ func processSpec(spec *APISpec, apisByListen map[string]int, handleCORS(&chainArray, spec) for _, obj := range mwPreFuncs { - if mwDriver != apidef.OttoDriver { + if mwDriver != apidef.OttoDriver && mwDriver != apidef.GojaDriver { coprocessLog.Debug("Registering coprocess middleware, hook name: ", obj.Name, "hook type: Pre", ", driver: ", mwDriver) mwAppendEnabled(&chainArray, &CoProcessMiddleware{baseMid, coprocess.HookType_Pre, obj.Name, mwDriver}) @@ -307,7 +307,7 @@ func processSpec(spec *APISpec, apisByListen map[string]int, mwAppendEnabled(&chainArray, &TransformMethod{BaseMiddleware: baseMid}) for _, obj := range mwPostFuncs { - if mwDriver != apidef.OttoDriver { + if mwDriver != apidef.OttoDriver && mwDriver != apidef.GojaDriver { coprocessLog.Debug("Registering coprocess middleware, hook name: ", obj.Name, "hook type: Post", ", driver: ", mwDriver) mwAppendEnabled(&chainArray, &CoProcessMiddleware{baseMid, coprocess.HookType_Post, obj.Name, mwDriver}) @@ -327,7 +327,7 @@ func processSpec(spec *APISpec, apisByListen map[string]int, // Add pre-process MW for _, obj := range mwPreFuncs { - if mwDriver != apidef.OttoDriver { + if mwDriver != apidef.OttoDriver && mwDriver != apidef.GojaDriver { coprocessLog.Debug("Registering coprocess middleware, hook name: ", obj.Name, "hook type: Pre", ", driver: ", mwDriver) mwAppendEnabled(&chainArray, &CoProcessMiddleware{baseMid, coprocess.HookType_Pre, obj.Name, mwDriver}) @@ -367,8 +367,8 @@ func processSpec(spec *APISpec, apisByListen map[string]int, logger.Info("Checking security policy: OpenID") } - coprocessAuth := EnableCoProcess && mwDriver != apidef.OttoDriver && spec.EnableCoProcessAuth - ottoAuth := !coprocessAuth && mwDriver == apidef.OttoDriver && spec.EnableCoProcessAuth + coprocessAuth := EnableCoProcess && mwDriver != apidef.OttoDriver && mwDriver != apidef.GojaDriver && spec.EnableCoProcessAuth + jsvmAuth := !coprocessAuth && (mwDriver == apidef.OttoDriver || mwDriver == apidef.GojaDriver) && spec.EnableCoProcessAuth if coprocessAuth { // TODO: check if mwAuthCheckFunc is available/valid @@ -378,7 +378,7 @@ func processSpec(spec *APISpec, apisByListen map[string]int, mwAppendEnabled(&authArray, &CoProcessMiddleware{baseMid, coprocess.HookType_CustomKeyCheck, mwAuthCheckFunc.Name, mwDriver}) } - if ottoAuth { + if jsvmAuth { logger.Info("----> Checking security policy: JS Plugin") @@ -414,7 +414,7 @@ func processSpec(spec *APISpec, apisByListen map[string]int, mwAppendEnabled(&chainArray, &VirtualEndpoint{BaseMiddleware: baseMid}) for _, obj := range mwPostFuncs { - if mwDriver != apidef.OttoDriver { + if mwDriver != apidef.OttoDriver && mwDriver != apidef.GojaDriver { coprocessLog.Debug("Registering coprocess middleware, hook name: ", obj.Name, "hook type: Post", ", driver: ", mwDriver) mwAppendEnabled(&chainArray, &CoProcessMiddleware{baseMid, coprocess.HookType_Post, obj.Name, mwDriver}) diff --git a/apidef/api_definitions.go b/apidef/api_definitions.go index 44f66312030..63e06fcc63e 100644 --- a/apidef/api_definitions.go +++ b/apidef/api_definitions.go @@ -39,6 +39,7 @@ const ( RequestJSON RequestInputType = "json" OttoDriver MiddlewareDriver = "otto" + GojaDriver MiddlewareDriver = "goja" PythonDriver MiddlewareDriver = "python" LuaDriver MiddlewareDriver = "lua" GrpcDriver MiddlewareDriver = "grpc" diff --git a/coprocess_bundle.go b/coprocess_bundle.go index a6d4539568d..f1f1f61d691 100644 --- a/coprocess_bundle.go +++ b/coprocess_bundle.go @@ -98,7 +98,7 @@ func (b *Bundle) AddToSpec() { b.Spec.CustomMiddleware = b.Manifest.CustomMiddleware // Call HandleMiddlewareCache only when using rich plugins: - if GlobalDispatcher != nil && b.Spec.CustomMiddleware.Driver != apidef.OttoDriver { + if GlobalDispatcher != nil && (b.Spec.CustomMiddleware.Driver != apidef.OttoDriver && b.Spec.CustomMiddleware.Driver != apidef.GojaDriver) { GlobalDispatcher.HandleMiddlewareCache(&b.Manifest, b.Path) } } diff --git a/coprocess_test.go b/coprocess_test.go index 53cdcef3f2d..32339e58d8a 100644 --- a/coprocess_test.go +++ b/coprocess_test.go @@ -11,6 +11,7 @@ import ( "net/url" "testing" + "github.com/Sirupsen/logrus" "github.com/golang/protobuf/proto" "github.com/justinas/alice" diff --git a/jsvm_goja.go b/jsvm_goja.go index be8fa2ad72f..85abd7542e6 100644 --- a/jsvm_goja.go +++ b/jsvm_goja.go @@ -137,7 +137,7 @@ func (j *GojaJSVM) RunJSRequestDynamic(d *DynamicMiddleware, logger *logrus.Entr // the whole Go program. recover() }() - returnRaw, err := vm.RunString(middlewareClassname + `.DoProcessRequest(` + string(requestAsJson) + `, ` + string(sessionAsJson) + `, ` + specAsJson + `);`) + returnRaw, err := vm.RunString(middlewareClassname + `.DoProcessRequest(` + requestAsJson + `, ` + sessionAsJson + `, ` + specAsJson + `);`) ret <- returnRaw errRet <- err }() @@ -429,3 +429,8 @@ func (j *GojaJSVM) Run(s string) (interface{}, error) { return j.VM.RunString(s) } + +// wraps goja String() function to avoid using reflection in functions/tests when stringifying results of vm.Run() - so do it here where its safer to assume type +func (j *GojaJSVM) String(val interface{}) string { + return val.(goja.Value).String() +} diff --git a/jsvm_otto.go b/jsvm_otto.go index 8a684285dcd..bf6fbb99c16 100644 --- a/jsvm_otto.go +++ b/jsvm_otto.go @@ -21,8 +21,6 @@ import ( "github.com/Sirupsen/logrus" ) -// --- Utility functions during startup to ensure a sane VM is present for each API Def ---- - type OttoJSVM struct { Spec *APISpec VM *otto.Otto @@ -124,6 +122,7 @@ func (j *OttoJSVM) RunJSRequestDynamic(d *DynamicMiddleware, logger *logrus.Entr recover() }() returnRaw, err := vm.Run(middlewareClassname + `.DoProcessRequest(` + string(requestAsJson) + `, ` + string(sessionAsJson) + `, ` + specAsJson + `);`) + ret <- returnRaw errRet <- err }() @@ -435,83 +434,7 @@ func (j *OttoJSVM) Run(s string) (interface{}, error) { return j.VM.Run(s) } -const coreJS = ` -var TykJS = { - TykMiddleware: { - MiddlewareComponentMeta: function(configuration) { - this.configuration = configuration - } - }, - TykEventHandlers: { - EventHandlerComponentMeta: function() {} - } -} - -TykJS.TykMiddleware.MiddlewareComponentMeta.prototype.ProcessRequest = function(request, session, config) { - log("Process Request Not Implemented") - return request -} - -TykJS.TykMiddleware.MiddlewareComponentMeta.prototype.DoProcessRequest = function(request, session, config) { - request.Body = b64dec(request.Body) - var processed_request = this.ProcessRequest(request, session, config) - - if (!processed_request) { - log("Middleware didn't return request object!") - return - } - - // Reset the headers object - processed_request.Request.Headers = {} - processed_request.Request.Body = b64enc(processed_request.Request.Body) - - return JSON.stringify(processed_request) -} - -// The user-level middleware component -TykJS.TykMiddleware.NewMiddleware = function(configuration) { - TykJS.TykMiddleware.MiddlewareComponentMeta.call(this, configuration) +// wraps otto string function to avoid using reflection in tests etc when stringifying results of vm.Run() so it here where its safer +func (j *OttoJSVM) String(val interface{}) string { + return val.(otto.Value).String() } - -// Set up object inheritance -TykJS.TykMiddleware.NewMiddleware.prototype = Object.create(TykJS.TykMiddleware.MiddlewareComponentMeta.prototype) -TykJS.TykMiddleware.NewMiddleware.prototype.constructor = TykJS.TykMiddleware.NewMiddleware - -TykJS.TykMiddleware.NewMiddleware.prototype.NewProcessRequest = function(callback) { - this.ProcessRequest = callback -} - -TykJS.TykMiddleware.NewMiddleware.prototype.ReturnData = function(request, session) { - return {Request: request, SessionMeta: session} -} - -TykJS.TykMiddleware.NewMiddleware.prototype.ReturnAuthData = function(request, session) { - return {Request: request, Session: session} -} - -// Event Handler implementation - -TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype.DoProcessEvent = function(event, context) { - // call the handler - log("Calling built - in handle") - this.Handle(event, context) - return -} - -TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype.Handle = function(request, context) { - log("Handler not implemented!") - return request -} - -// The user-level event handler component -TykJS.TykEventHandlers.NewEventHandler = function() { - TykJS.TykEventHandlers.EventHandlerComponentMeta.call(this) -} - -// Set up object inheritance for events -TykJS.TykEventHandlers.NewEventHandler.prototype = Object.create(TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype) -TykJS.TykEventHandlers.NewEventHandler.prototype.constructor = TykJS.TykEventHandlers.NewEventHandler - -TykJS.TykEventHandlers.NewEventHandler.prototype.NewHandler = function(callback) { - this.Handle = callback -};` diff --git a/main.go b/main.go index 45bee4cd34a..90078dba54e 100644 --- a/main.go +++ b/main.go @@ -483,13 +483,17 @@ func loadCustomMiddleware(spec *APISpec) ([]string, apidef.MiddlewareDefinition, mwPreFuncs := []apidef.MiddlewareDefinition{} mwPostFuncs := []apidef.MiddlewareDefinition{} mwPostKeyAuthFuncs := []apidef.MiddlewareDefinition{} - mwDriver := apidef.OttoDriver - + var mwDriver apidef.MiddlewareDriver + if config.Global().JSVM == "goja" { + mwDriver = apidef.GojaDriver + } else { + mwDriver = apidef.OttoDriver + } // Set AuthCheck hook if spec.CustomMiddleware.AuthCheck.Name != "" { mwAuthCheckFunc = spec.CustomMiddleware.AuthCheck if spec.CustomMiddleware.AuthCheck.Path != "" { - // Feed a JS file to Otto + // Feed a JS file to JSVM mwPaths = append(mwPaths, spec.CustomMiddleware.AuthCheck.Path) } } @@ -553,7 +557,7 @@ func loadCustomMiddleware(spec *APISpec) ([]string, apidef.MiddlewareDefinition, // Load PostAuthCheck hooks for _, mwObj := range spec.CustomMiddleware.PostKeyAuth { if mwObj.Path != "" { - // Otto files are specified here + // JSVM files are specified here mwPaths = append(mwPaths, mwObj.Path) } mwPostKeyAuthFuncs = append(mwPostKeyAuthFuncs, mwObj) diff --git a/mw_js_plugin.go b/mw_js_plugin.go index 1bc590eea4f..a33d16a283e 100644 --- a/mw_js_plugin.go +++ b/mw_js_plugin.go @@ -141,10 +141,11 @@ func (d *DynamicMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Reques // Run the middleware - err, code, returnDataStr := d.Spec.JSVM.RunJSRequestDynamic(d, logger, string(requestAsJson), string(specAsJson), string(sessionAsJson)) + err, code, returnDataStr := d.Spec.JSVM.RunJSRequestDynamic(d, logger, string(requestAsJson), string(sessionAsJson), specAsJson) if err != nil { - //TODO + log.Errorf("JSVM error: %v", err) } + if code != -1 { return nil, code } @@ -241,3 +242,84 @@ func mapStrsToIfaces(m map[string]string) map[string]interface{} { } return m2 } + +const coreJS = ` +var TykJS = { + TykMiddleware: { + MiddlewareComponentMeta: function(configuration) { + this.configuration = configuration + } + }, + TykEventHandlers: { + EventHandlerComponentMeta: function() {} + } +} + +TykJS.TykMiddleware.MiddlewareComponentMeta.prototype.ProcessRequest = function(request, session, config) { + log("Process Request Not Implemented") + return request +} + +TykJS.TykMiddleware.MiddlewareComponentMeta.prototype.DoProcessRequest = function(request, session, config) { + request.Body = b64dec(request.Body) + var processed_request = this.ProcessRequest(request, session, config) + + if (!processed_request) { + log("Middleware didn't return request object!") + return + } + + // Reset the headers object + processed_request.Request.Headers = {} + processed_request.Request.Body = b64enc(processed_request.Request.Body) + + return JSON.stringify(processed_request) +} + +// The user-level middleware component +TykJS.TykMiddleware.NewMiddleware = function(configuration) { + TykJS.TykMiddleware.MiddlewareComponentMeta.call(this, configuration) +} + +// Set up object inheritance +TykJS.TykMiddleware.NewMiddleware.prototype = Object.create(TykJS.TykMiddleware.MiddlewareComponentMeta.prototype) +TykJS.TykMiddleware.NewMiddleware.prototype.constructor = TykJS.TykMiddleware.NewMiddleware + +TykJS.TykMiddleware.NewMiddleware.prototype.NewProcessRequest = function(callback) { + this.ProcessRequest = callback +} + +TykJS.TykMiddleware.NewMiddleware.prototype.ReturnData = function(request, session) { + return {Request: request, SessionMeta: session} +} + +TykJS.TykMiddleware.NewMiddleware.prototype.ReturnAuthData = function(request, session) { + return {Request: request, Session: session} +} + +// Event Handler implementation + +TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype.DoProcessEvent = function(event, context) { + // call the handler + log("Calling built - in handle") + this.Handle(event, context) + return +} + +TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype.Handle = function(request, context) { + log("Handler not implemented!") + return request +} + +// The user-level event handler component +TykJS.TykEventHandlers.NewEventHandler = function() { + TykJS.TykEventHandlers.EventHandlerComponentMeta.call(this) +} + +// Set up object inheritance for events +TykJS.TykEventHandlers.NewEventHandler.prototype = Object.create(TykJS.TykEventHandlers.EventHandlerComponentMeta.prototype) +TykJS.TykEventHandlers.NewEventHandler.prototype.constructor = TykJS.TykEventHandlers.NewEventHandler + +TykJS.TykEventHandlers.NewEventHandler.prototype.NewHandler = function(callback) { + this.Handle = callback +};` diff --git a/mw_js_plugin_test.go b/mw_js_plugin_test.go index 302dfb113f7..ae2ccf764f7 100644 --- a/mw_js_plugin_test.go +++ b/mw_js_plugin_test.go @@ -21,7 +21,7 @@ import ( "github.com/TykTechnologies/tyk/test" ) -func TestJSVMLogs(t *testing.T) { +func TestJSVMLogsOtto(t *testing.T) { var buf bytes.Buffer log := logrus.New() log.Out = &buf @@ -71,7 +71,56 @@ rawlog('{"x": "y"}') } } -func TestJSVMBody(t *testing.T) { +func TestJSVMLogsGoja(t *testing.T) { + var buf bytes.Buffer + log := logrus.New() + log.Out = &buf + log.Formatter = new(prefixed.TextFormatter) + + jsvm := &GojaJSVM{} + jsvm.Init(nil, logrus.NewEntry(log)) + + jsvm.RawLog = logrus.New() + jsvm.RawLog.Out = &buf + jsvm.RawLog.Formatter = new(logger.RawFormatter) + + const in = ` +log("foo") +log('{"x": "y"}') +rawlog("foo") +rawlog('{"x": "y"}') +` + // note how the logger leaves spaces at the end + want := []string{ + `time=TIME level=info msg=foo type=log-msg `, + `time=TIME level=info msg="{\"x\": \"y\"}" type=log-msg `, + `foo`, + `{"x": "y"}`, + } + if _, err := jsvm.Run(in); err != nil { + t.Fatalf("failed to run js: %v", err) + } + got := strings.Split(strings.Trim(buf.String(), "\n"), "\n") + i := 0 + timeRe := regexp.MustCompile(`time="[^"]*"`) + for _, line := range got { + if i >= len(want) { + t.Logf("too many lines") + t.Fail() + break + } + s := timeRe.ReplaceAllString(line, "time=TIME") + if s != line && !strings.Contains(s, "type=log-msg") { + continue // log line from elsewhere (async) + } + if s != want[i] { + t.Logf("%s != %s", s, want[i]) + t.Fail() + } + i++ + } +} +func TestJSVMBodyOtto(t *testing.T) { dynMid := &DynamicMiddleware{ BaseMiddleware: BaseMiddleware{ Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}}, @@ -92,7 +141,47 @@ leakMid.NewProcessRequest(function(request, session) { request.Body += " appended" return leakMid.ReturnData(request, session.meta_data) });` - if _, err := jsvm.VM.Run(js); err != nil { + //run with otto + if _, err := jsvm.Run(js); err != nil { + t.Fatalf("failed to set up js plugin: %v", err) + } + dynMid.Spec.JSVM = jsvm + dynMid.ProcessRequest(nil, req, nil) + + bs, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatalf("failed to read final body: %v", err) + } + want := body + " appended" + if got := string(bs); want != got { + t.Fatalf("JS plugin broke non-UTF8 body %q into %q", + want, got) + } +} +func TestJSVMBodyGoja(t *testing.T) { + dynMid := &DynamicMiddleware{ + BaseMiddleware: BaseMiddleware{ + Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}}, + }, + MiddlewareClassName: "leakMid", + Pre: true, + } + //different body from otto test because Goja will actually render \xff as � character + body := "foô \uffff \u0000 bàr" + req := httptest.NewRequest("GET", "/foo", strings.NewReader(body)) + jsvm := &GojaJSVM{} + + jsvm.Init(nil, logrus.NewEntry(log)) + + const js = ` +var leakMid = new TykJS.TykMiddleware.NewMiddleware({}) + +leakMid.NewProcessRequest(function(request, session) { + request.Body += " appended" + return leakMid.ReturnData(request, session.meta_data) +});` + //run with goja + if _, err := jsvm.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) } dynMid.Spec.JSVM = jsvm @@ -109,7 +198,7 @@ leakMid.NewProcessRequest(function(request, session) { } } -func TestJSVMProcessTimeout(t *testing.T) { +func TestJSVMProcessTimeoutOtto(t *testing.T) { dynMid := &DynamicMiddleware{ BaseMiddleware: BaseMiddleware{ Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}}, @@ -132,7 +221,7 @@ leakMid.NewProcessRequest(function(request, session) { } return leakMid.ReturnData(request, session.meta_data) });` - if _, err := jsvm.VM.Run(js); err != nil { + if _, err := jsvm.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) } dynMid.Spec.JSVM = jsvm @@ -149,7 +238,47 @@ leakMid.NewProcessRequest(function(request, session) { } } -func TestJSVMConfigData(t *testing.T) { +func TestJSVMProcessTimeoutGoja(t *testing.T) { + dynMid := &DynamicMiddleware{ + BaseMiddleware: BaseMiddleware{ + Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}}, + }, + MiddlewareClassName: "leakMid", + Pre: true, + } + req := httptest.NewRequest("GET", "/foo", strings.NewReader("body")) + jsvm := &GojaJSVM{} + jsvm.Init(nil, logrus.NewEntry(log)) + jsvm.Timeout = time.Millisecond + + // this js plugin just loops forever, keeping Otto at 100% CPU + // usage and running forever. + const js = ` +var leakMid = new TykJS.TykMiddleware.NewMiddleware({}) + +leakMid.NewProcessRequest(function(request, session) { + while (true) { + } + return leakMid.ReturnData(request, session.meta_data) +});` + if _, err := jsvm.Run(js); err != nil { + t.Fatalf("failed to set up js plugin: %v", err) + } + dynMid.Spec.JSVM = jsvm + + done := make(chan bool) + go func() { + dynMid.ProcessRequest(nil, req, nil) + done <- true + }() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("js vm wasn't killed after its timeout") + } +} + +func TestJSVMConfigDataOtto(t *testing.T) { spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} spec.ConfigData = map[string]interface{}{ "foo": "bar", @@ -158,6 +287,7 @@ func TestJSVMConfigData(t *testing.T) { var testJSVMData = new TykJS.TykMiddleware.NewMiddleware({}) testJSVMData.NewProcessRequest(function(request, session, spec) { + request.SetHeaders["data-foo"] = spec.config_data.foo return testJSVMData.ReturnData(request, {}) });` @@ -168,7 +298,39 @@ testJSVMData.NewProcessRequest(function(request, session, spec) { } jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) - if _, err := jsvm.VM.Run(js); err != nil { + if _, err := jsvm.Run(js); err != nil { + t.Fatalf("failed to set up js plugin: %v", err) + } + dynMid.Spec.JSVM = jsvm + + r := testReq(t, "GET", "/v1/test-data", nil) + dynMid.ProcessRequest(nil, r, nil) + if want, got := "bar", r.Header.Get("data-foo"); want != got { + t.Fatalf("wanted header to be %q, got %q", want, got) + } +} + +func TestJSVMConfigDataGoja(t *testing.T) { + spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} + spec.ConfigData = map[string]interface{}{ + "foo": "bar", + } + const js = ` +var testJSVMData = new TykJS.TykMiddleware.NewMiddleware({}) + +testJSVMData.NewProcessRequest(function(request, session, spec) { + + request.SetHeaders["data-foo"] = spec.config_data.foo + return testJSVMData.ReturnData(request, {}) +});` + dynMid := &DynamicMiddleware{ + BaseMiddleware: BaseMiddleware{Spec: spec, Proxy: nil}, + MiddlewareClassName: "testJSVMData", + Pre: true, + } + jsvm := &GojaJSVM{} + jsvm.Init(nil, logrus.NewEntry(log)) + if _, err := jsvm.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) } dynMid.Spec.JSVM = jsvm @@ -180,7 +342,7 @@ testJSVMData.NewProcessRequest(function(request, session, spec) { } } -func TestJSVMReturnOverridesFullResponse(t *testing.T) { +func TestJSVMReturnOverridesFullResponseOtto(t *testing.T) { spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} spec.ConfigData = map[string]interface{}{ "foo": "bar", @@ -204,7 +366,57 @@ testJSVMData.NewProcessRequest(function(request, session, config) { } jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) - if _, err := jsvm.VM.Run(js); err != nil { + if _, err := jsvm.Run(js); err != nil { + t.Fatalf("failed to set up js plugin: %v", err) + } + dynMid.Spec.JSVM = jsvm + + rec := httptest.NewRecorder() + r := testReq(t, "GET", "/v1/test-data", nil) + dynMid.ProcessRequest(rec, r, nil) + + wantBody := "Foobarbaz" + gotBody := rec.Body.String() + if wantBody != gotBody { + t.Fatalf("wanted body to be %q, got %q", wantBody, gotBody) + } + if want, got := "Bar", rec.HeaderMap.Get("x-foo"); got != want { + t.Fatalf("wanted header to be %q, got %q", want, got) + } + if want, got := "Qux", rec.HeaderMap.Get("x-baz"); got != want { + t.Fatalf("wanted header to be %q, got %q", want, got) + } + + if want := 200; rec.Code != 200 { + t.Fatalf("wanted code to be %d, got %d", want, rec.Code) + } +} + +func TestJSVMReturnOverridesFullResponseGoja(t *testing.T) { + spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} + spec.ConfigData = map[string]interface{}{ + "foo": "bar", + } + const js = ` +var testJSVMData = new TykJS.TykMiddleware.NewMiddleware({}) + +testJSVMData.NewProcessRequest(function(request, session, config) { + request.ReturnOverrides.ResponseError = "Foobarbaz" + request.ReturnOverrides.ResponseCode = 200 + request.ReturnOverrides.ResponseHeaders = { + "X-Foo": "Bar", + "X-Baz": "Qux" + } + return testJSVMData.ReturnData(request, {}) +});` + dynMid := &DynamicMiddleware{ + BaseMiddleware: BaseMiddleware{Spec: spec, Proxy: nil}, + MiddlewareClassName: "testJSVMData", + Pre: true, + } + jsvm := &GojaJSVM{} + jsvm.Init(nil, logrus.NewEntry(log)) + if _, err := jsvm.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) } dynMid.Spec.JSVM = jsvm @@ -230,7 +442,7 @@ testJSVMData.NewProcessRequest(function(request, session, config) { } } -func TestJSVMReturnOverridesError(t *testing.T) { +func TestJSVMReturnOverridesErrorOtto(t *testing.T) { spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} spec.ConfigData = map[string]interface{}{ "foo": "bar", @@ -250,7 +462,7 @@ testJSVMData.NewProcessRequest(function(request, session, config) { } jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) - if _, err := jsvm.VM.Run(js); err != nil { + if _, err := jsvm.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) } dynMid.Spec.JSVM = jsvm @@ -268,7 +480,44 @@ testJSVMData.NewProcessRequest(function(request, session, config) { } } -func TestJSVMUserCore(t *testing.T) { +func TestJSVMReturnOverridesErrorGoja(t *testing.T) { + spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} + spec.ConfigData = map[string]interface{}{ + "foo": "bar", + } + const js = ` +var testJSVMData = new TykJS.TykMiddleware.NewMiddleware({}) + +testJSVMData.NewProcessRequest(function(request, session, config) { + request.ReturnOverrides.ResponseError = "Foobarbaz" + request.ReturnOverrides.ResponseCode = 401 + return testJSVMData.ReturnData(request, {}) +});` + dynMid := &DynamicMiddleware{ + BaseMiddleware: BaseMiddleware{Spec: spec, Proxy: nil}, + MiddlewareClassName: "testJSVMData", + Pre: true, + } + jsvm := &GojaJSVM{} + jsvm.Init(nil, logrus.NewEntry(log)) + if _, err := jsvm.Run(js); err != nil { + t.Fatalf("failed to set up js plugin: %v", err) + } + dynMid.Spec.JSVM = jsvm + + r := testReq(t, "GET", "/v1/test-data", nil) + err, code := dynMid.ProcessRequest(nil, r, nil) + + if want := 401; code != 401 { + t.Fatalf("wanted code to be %d, got %d", want, code) + } + + wantBody := "Foobarbaz" + if !strings.Contains(err.Error(), wantBody) { + t.Fatalf("wanted body to contain to be %v, got %v", wantBody, err.Error()) + } +} +func TestJSVMUserCoreOtto(t *testing.T) { spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} const js = ` var testJSVMCore = new TykJS.TykMiddleware.NewMiddleware({}) @@ -299,7 +548,7 @@ testJSVMCore.NewProcessRequest(function(request, session, config) { }() jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) - if _, err := jsvm.VM.Run(js); err != nil { + if _, err := jsvm.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) } dynMid.Spec.JSVM = jsvm @@ -312,7 +561,50 @@ testJSVMCore.NewProcessRequest(function(request, session, config) { } } -func TestJSVMRequestScheme(t *testing.T) { +func TestJSVMUserCoreGoja(t *testing.T) { + spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} + const js = ` +var testJSVMCore = new TykJS.TykMiddleware.NewMiddleware({}) + +testJSVMCore.NewProcessRequest(function(request, session, config) { + request.SetHeaders["global"] = globalVar + return testJSVMCore.ReturnData(request, {}) +});` + dynMid := &DynamicMiddleware{ + BaseMiddleware: BaseMiddleware{Spec: spec, Proxy: nil}, + MiddlewareClassName: "testJSVMCore", + Pre: true, + } + tfile, err := ioutil.TempFile("", "tykjs") + if err != nil { + t.Fatal(err) + } + if _, err := io.WriteString(tfile, `var globalVar = "globalValue"`); err != nil { + t.Fatal(err) + } + globalConf := config.Global() + old := globalConf.TykJSPath + globalConf.TykJSPath = tfile.Name() + config.SetGlobal(globalConf) + defer func() { + globalConf.TykJSPath = old + config.SetGlobal(globalConf) + }() + jsvm := &GojaJSVM{} + jsvm.Init(nil, logrus.NewEntry(log)) + if _, err := jsvm.Run(js); err != nil { + t.Fatalf("failed to set up js plugin: %v", err) + } + dynMid.Spec.JSVM = jsvm + + r := testReq(t, "GET", "/foo", nil) + dynMid.ProcessRequest(nil, r, nil) + + if want, got := "globalValue", r.Header.Get("global"); want != got { + t.Fatalf("wanted header to be %q, got %q", want, got) + } +} +func TestJSVMRequestSchemeOtto(t *testing.T) { dynMid := &DynamicMiddleware{ BaseMiddleware: BaseMiddleware{ Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}}, @@ -334,7 +626,47 @@ leakMid.NewProcessRequest(function(request, session) { } return leakMid.ReturnData(responseObject, session.meta_data) });` - if _, err := jsvm.VM.Run(js); err != nil { + if _, err := jsvm.Run(js); err != nil { + t.Fatalf("failed to set up js plugin: %v", err) + } + dynMid.Spec.JSVM = jsvm + dynMid.ProcessRequest(nil, req, nil) + + bs, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatalf("failed to read final body: %v", err) + } + want := "http" + " appended" + if got := string(bs); want != got { + t.Fatalf("JS plugin broke non-UTF8 body %q into %q", + want, got) + } +} + +func TestJSVMRequestSchemeGoja(t *testing.T) { + dynMid := &DynamicMiddleware{ + BaseMiddleware: BaseMiddleware{ + Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}}, + }, + MiddlewareClassName: "leakMid", + Pre: true, + } + req := httptest.NewRequest("GET", "/foo", nil) + req.URL.Scheme = "http" + jsvm := &GojaJSVM{} + jsvm.Init(nil, logrus.NewEntry(log)) + + const js = ` +var leakMid = new TykJS.TykMiddleware.NewMiddleware({}) +leakMid.NewProcessRequest(function(request, session) { + var test = request.Scheme += " appended" + var responseObject = { + Body: test, + Code: 200 + } + return leakMid.ReturnData(responseObject, session.meta_data) +});` + if _, err := jsvm.Run(js); err != nil { t.Fatalf("failed to set up js plugin: %v", err) } dynMid.Spec.JSVM = jsvm @@ -351,7 +683,10 @@ leakMid.NewProcessRequest(function(request, session) { } } -func TestTykMakeHTTPRequest(t *testing.T) { +func TestTykMakeHTTPRequestOtto(t *testing.T) { + globalConf := config.Global() + globalConf.JSVM = "otto" + config.SetGlobal(globalConf) ts := newTykTestServer() defer ts.Close() @@ -463,7 +798,77 @@ func TestTykMakeHTTPRequest(t *testing.T) { }) } -func TestJSVMBase64(t *testing.T) { +func TestTykMakeHTTPRequestGoja(t *testing.T) { + globalConf := config.Global() + globalConf.JSVM = "goja" + config.SetGlobal(globalConf) + ts := newTykTestServer() + defer ts.Close() + + bundle := registerBundle("jsvm_make_http_request", map[string]string{ + "manifest.json": ` + { + "file_list": [], + "custom_middleware": { + "driver": "goja", + "pre": [{ + "name": "testTykMakeHTTPRequest", + "path": "middleware.js" + }] + } + } + `, + "middleware.js": ` + var testTykMakeHTTPRequest = new TykJS.TykMiddleware.NewMiddleware({}) + + testTykMakeHTTPRequest.NewProcessRequest(function(request, session, spec) { + var newRequest = { + "Method": "GET", + "Headers": {"Accept": "application/json"}, + "Domain": spec.config_data.base_url, + "Resource": "/api/get" + } + + var resp = TykMakeHttpRequest(JSON.stringify(newRequest)); + var useableResponse = JSON.parse(resp); + + if(useableResponse.Code > 400) { + request.ReturnOverrides.ResponseCode = useableResponse.code + request.ReturnOverrides.ResponseError = "error" + } + + return testTykMakeHTTPRequest.ReturnData(request, {}) + }); + `}) + + t.Run("Existing endpoint", func(t *testing.T) { + buildAndLoadAPI(func(spec *APISpec) { + spec.Proxy.ListenPath = "/sample" + spec.ConfigData = map[string]interface{}{ + "base_url": ts.URL, + } + spec.CustomMiddlewareBundle = bundle + }, func(spec *APISpec) { + spec.Proxy.ListenPath = "/api" + }) + + ts.Run(t, test.TestCase{Path: "/sample", Code: 200}) + }) + + t.Run("Nonexistent endpoint", func(t *testing.T) { + buildAndLoadAPI(func(spec *APISpec) { + spec.Proxy.ListenPath = "/sample" + spec.ConfigData = map[string]interface{}{ + "base_url": ts.URL, + } + spec.CustomMiddlewareBundle = bundle + }) + + ts.Run(t, test.TestCase{Path: "/sample", Code: 404}) + }) +} + +func TestJSVMBase64Otto(t *testing.T) { jsvm := OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) @@ -473,52 +878,113 @@ func TestJSVMBase64(t *testing.T) { decodedJwtPayload := `{"sub":"1234567890","name":"John Doe","iat":1516239022}` t.Run("b64dec with simple string input", func(t *testing.T) { - v, err := jsvm.VM.Run(`b64dec("` + inputB64 + `")`) + v, err := jsvm.Run(`b64dec("` + inputB64 + `")`) + if err != nil { + t.Fatalf("b64dec call failed: %s", err.Error()) + } + if s := jsvm.String(v); s != inputString { + t.Fatalf("wanted '%s', got '%s'", inputString, s) + } + }) + + t.Run("b64dec with a JWT payload", func(t *testing.T) { + v, err := jsvm.Run(`b64dec("` + jwtPayload + `")`) + if err != nil { + t.Fatalf("b64dec call failed: %s", err.Error()) + } + if s := jsvm.String(v); s != decodedJwtPayload { + t.Fatalf("wanted '%s', got '%s'", decodedJwtPayload, s) + } + }) + + t.Run("b64enc with simple string input", func(t *testing.T) { + v, err := jsvm.Run(`b64enc("` + inputString + `")`) + if err != nil { + t.Fatalf("b64enc call failed: %s", err.Error()) + } + if s := jsvm.String(v); s != inputB64 { + t.Fatalf("wanted '%s', got '%s'", inputB64, s) + } + }) + + t.Run("rawb64dec with simple string input", func(t *testing.T) { + v, err := jsvm.Run(`rawb64dec("` + jwtPayload + `")`) + if err != nil { + t.Fatalf("rawb64dec call failed: %s", err.Error()) + } + if s := jsvm.String(v); s != decodedJwtPayload { + t.Fatalf("wanted '%s', got '%s'", decodedJwtPayload, s) + } + }) + + t.Run("rawb64enc with simple string input", func(t *testing.T) { + jsvm.VM.Set("input", decodedJwtPayload) + v, err := jsvm.Run(`rawb64enc(input)`) + if err != nil { + t.Fatalf("rawb64enc call failed: %s", err.Error()) + } + if s := jsvm.String(v); s != jwtPayload { + t.Fatalf("wanted '%s', got '%s'", jwtPayload, s) + } + }) +} + +func TestJSVMBase64Goja(t *testing.T) { + jsvm := GojaJSVM{} + jsvm.Init(nil, logrus.NewEntry(log)) + + inputString := "teststring" + inputB64 := "dGVzdHN0cmluZw==" + jwtPayload := "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" + decodedJwtPayload := `{"sub":"1234567890","name":"John Doe","iat":1516239022}` + + t.Run("b64dec with simple string input", func(t *testing.T) { + v, err := jsvm.Run(`b64dec("` + inputB64 + `")`) if err != nil { t.Fatalf("b64dec call failed: %s", err.Error()) } - if s := v.String(); s != inputString { + if s := jsvm.String(v); s != inputString { t.Fatalf("wanted '%s', got '%s'", inputString, s) } }) t.Run("b64dec with a JWT payload", func(t *testing.T) { - v, err := jsvm.VM.Run(`b64dec("` + jwtPayload + `")`) + v, err := jsvm.Run(`b64dec("` + jwtPayload + `")`) if err != nil { t.Fatalf("b64dec call failed: %s", err.Error()) } - if s := v.String(); s != decodedJwtPayload { + if s := jsvm.String(v); s != decodedJwtPayload { t.Fatalf("wanted '%s', got '%s'", decodedJwtPayload, s) } }) t.Run("b64enc with simple string input", func(t *testing.T) { - v, err := jsvm.VM.Run(`b64enc("` + inputString + `")`) + v, err := jsvm.Run(`b64enc("` + inputString + `")`) if err != nil { t.Fatalf("b64enc call failed: %s", err.Error()) } - if s := v.String(); s != inputB64 { + if s := jsvm.String(v); s != inputB64 { t.Fatalf("wanted '%s', got '%s'", inputB64, s) } }) t.Run("rawb64dec with simple string input", func(t *testing.T) { - v, err := jsvm.VM.Run(`rawb64dec("` + jwtPayload + `")`) + v, err := jsvm.Run(`rawb64dec("` + jwtPayload + `")`) if err != nil { t.Fatalf("rawb64dec call failed: %s", err.Error()) } - if s := v.String(); s != decodedJwtPayload { + if s := jsvm.String(v); s != decodedJwtPayload { t.Fatalf("wanted '%s', got '%s'", decodedJwtPayload, s) } }) t.Run("rawb64enc with simple string input", func(t *testing.T) { jsvm.VM.Set("input", decodedJwtPayload) - v, err := jsvm.VM.Run(`rawb64enc(input)`) + v, err := jsvm.Run(`rawb64enc(input)`) if err != nil { t.Fatalf("rawb64enc call failed: %s", err.Error()) } - if s := v.String(); s != jwtPayload { + if s := jsvm.String(v); s != jwtPayload { t.Fatalf("wanted '%s', got '%s'", jwtPayload, s) } }) From b6fcecac4b8e23bb26550bd3adf1750b0324ce8e Mon Sep 17 00:00:00 2001 From: joshblakeley Date: Tue, 6 Nov 2018 09:49:19 +0000 Subject: [PATCH 6/8] add jsvm to lint schema and update otto vendor --- cli/lint/schema.go | 766 ++++++++++++++++++ jsvm_goja.go | 14 +- jsvm_otto.go | 5 +- mw_js_plugin.go | 3 +- mw_virtual_endpoint.go | 5 +- mw_virtual_endpoint_test.go | 2 +- .../robertkrimen/otto/README.markdown | 2 +- .../robertkrimen/otto/builtin_array.go | 5 +- vendor/github.com/robertkrimen/otto/error.go | 1 + vendor/github.com/robertkrimen/otto/inline.pl | 14 +- .../github.com/robertkrimen/otto/runtime.go | 94 ++- .../robertkrimen/otto/type_function.go | 50 +- vendor/vendor.json | 6 +- 13 files changed, 938 insertions(+), 29 deletions(-) create mode 100644 cli/lint/schema.go diff --git a/cli/lint/schema.go b/cli/lint/schema.go new file mode 100644 index 00000000000..e881344bb0f --- /dev/null +++ b/cli/lint/schema.go @@ -0,0 +1,766 @@ +package lint + +const confSchema = `{ +"$schema": "http://json-schema.org/draft-04/schema#", +"type": "object", +"additionalProperties": false, +"definitions": { + "StorageOptions": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "database": { + "type": "integer" + }, + "enable_cluster": { + "type": "boolean" + }, + "use_ssl":{ + "type": "boolean" + }, + "ssl_insecure_skip_verify":{ + "type": "boolean" + }, + "host": { + "type": "string", + "format": "host-no-port" + }, + "hosts": { + "type": ["array", "null"] + }, + "optimisation_max_active": { + "type": "integer" + }, + "optimisation_max_idle": { + "type": "integer" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "type": { + "type": "string", + "enum": ["", "redis"] + }, + "username": { + "type": "string" + } + } + } +}, +"properties": { + "allow_insecure_configs": { + "type": "boolean" + }, + "allow_master_keys": { + "type": "boolean" + }, + "allow_remote_config": { + "type": "boolean" + }, + "analytics_config": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "enable_detailed_recording": { + "type": "boolean" + }, + "enable_geo_ip": { + "type": "boolean" + }, + "geo_ip_db_path": { + "type": "string", + "format": "path" + }, + "ignored_ips": { + "type": ["array", "null"] + }, + "normalise_urls": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "custom_patterns": { + "type": ["array", "null"] + }, + "enabled": { + "type": "boolean" + }, + "normalise_numbers": { + "type": "boolean" + }, + "normalise_uuids": { + "type": "boolean" + } + } + }, + "pool_size": { + "type": "integer" + }, + "records_buffer_size": { + "type": "integer" + }, + "storage_expiration_time": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + }, + "app_path": { + "type": "string", + "format": "path" + }, + "auth_override": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "auth_provider": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "meta": { + "type": ["array", "null"] + }, + "name": { + "type": "string" + }, + "storage_engine": { + "type": "string" + } + } + }, + "force_auth_provider": { + "type": "boolean" + }, + "force_session_provider": { + "type": "boolean" + }, + "session_provider": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "meta": { + "type": ["array", "null"] + }, + "name": { + "type": "string" + }, + "storage_engine": { + "type": "string" + } + } + } + } + }, + "bundle_base_url": { + "type": "string" + }, + "cache_storage": { + "$ref": "#/definitions/StorageOptions" + }, + "close_connections": { + "type": "boolean" + }, + "proxy_close_connections": { + "type": "boolean" + }, + "close_idle_connections": { + "type": "boolean" + }, + "control_api_hostname": { + "type": "string" + }, + "control_api_port": { + "type": "integer" + }, + "coprocess_options": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "coprocess_grpc_server": { + "type": "string" + }, + "enable_coprocess": { + "type": "boolean" + }, + "python_path_prefix": { + "type": "string" + } + } + }, + "db_app_conf_options": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "connection_string": { + "type": "string" + }, + "node_is_segmented": { + "type": "boolean" + }, + "tags": { + "type": ["array", "null"], + "items": { + "type": "string" + } + } + } + }, + "version_header":{ + "type": "string" + }, + "disable_dashboard_zeroconf": { + "type": "boolean" + }, + "disable_virtual_path_blobs": { + "type": "boolean" + }, + "drl_notification_frequency": { + "type": "integer" + }, + "enable_analytics": { + "type": "boolean" + }, + "enable_api_segregation": { + "type": "boolean" + }, + "enable_bundle_downloader": { + "type": "boolean" + }, + "enable_custom_domains": { + "type": "boolean" + }, + "enable_jsvm": { + "type": "boolean" + }, + "jsvm":{ + "type": "string" + }, + "jsvm_timeout": { + "type": "integer" + }, + "enable_non_transactional_rate_limiter": { + "type": "boolean" + }, + "enable_redis_rolling_limiter": { + "type": "boolean" + }, + "enable_sentinel_rate_limiter": { + "type": "boolean" + }, + "enable_separate_cache_store": { + "type": "boolean" + }, + "enforce_org_data_age": { + "type": "boolean" + }, + "enforce_org_data_detail_logging": { + "type": "boolean" + }, + "enforce_org_quotas": { + "type": "boolean" + }, + "event_handlers": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "events": { + "type": ["object", "null"], + "additionalProperties": false + } + } + }, + "event_trigers_defunct": { + "type": ["array", "null"] + }, + "experimental_process_org_off_thread": { + "type": "boolean" + }, + "force_global_session_lifetime": { + "type": "boolean" + }, + "global_session_lifetime": { + "type": "integer" + }, + "graylog_network_addr": { + "type": "string" + }, + "hash_keys": { + "type": "boolean" + }, + "hash_key_function": { + "type": "string", + "enum": ["", "murmur32", "murmur64", "murmur128", "sha256"] + }, + "health_check": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "enable_health_checks": { + "type": "boolean" + }, + "health_check_value_timeouts": { + "type": "integer" + } + } + }, + "hide_generator_header": { + "type": "boolean" + }, + "hostname": { + "type": "string" + }, + "http_server_options": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "certificates": { + "type": ["array", "null"], + "items": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "domain_name": { + "type": "string" + }, + "cert_file": { + "type": "string" + }, + "key_file": { + "type": "string" + } + } + } + }, + "enable_websockets": { + "type": "boolean" + }, + "flush_interval": { + "type": "integer" + }, + "min_version": { + "type": "integer" + }, + "override_defaults": { + "type": "boolean" + }, + "read_timeout": { + "type": "integer" + }, + "server_name": { + "type": "string" + }, + "skip_url_cleaning": { + "type": "boolean" + }, + "skip_target_path_escaping": { + "type": "boolean" + }, + "ssl_insecure_skip_verify": { + "type": "boolean" + }, + "use_ssl": { + "type": "boolean" + }, + "use_ssl_le": { + "type": "boolean" + }, + "write_timeout": { + "type": "integer" + }, + "ssl_certificates": { + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "ssl_ciphers":{ + "type": ["array", "null"], + "items": { + "type": "string" + } + } + } + }, + "legacy_enable_allowance_countdown": { + "type": "boolean" + }, + "listen_address": { + "type": "string", + "format": "host-no-port" + }, + "listen_port": { + "type": "integer" + }, + "local_session_cache": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "cached_session_eviction": { + "type": "integer" + }, + "cached_session_timeout": { + "type": "integer" + }, + "disable_cached_session_state": { + "type": "boolean" + } + } + }, + "log_level": { + "type": "string", + "enum": ["", "debug", "info", "warn", "error"] + }, + "logstash_network_addr": { + "type": "string" + }, + "logstash_transport": { + "type": "string" + }, + "management_node": { + "type": "boolean" + }, + "max_idle_connections_per_host": { + "type": "integer" + }, + "max_idle_connections": { + "type": "integer" + }, + "max_conn_time": { + "type": "integer" + }, + "middleware_path": { + "type": "string", + "format": "path" + }, + "monitor": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "configuration": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "event_timeout": { + "type": "integer" + }, + "header_map": { + "type": ["array", "null"] + }, + "method": { + "type": "string" + }, + "target_path": { + "type": "string" + }, + "template_path": { + "type": "string", + "format": "path" + } + } + }, + "enable_trigger_monitors": { + "type": "boolean" + }, + "global_trigger_limit": { + "type": "integer" + }, + "monitor_org_keys": { + "type": "boolean" + }, + "monitor_user_keys": { + "type": "boolean" + } + } + }, + "node_secret": { + "type": "string" + }, + "oauth_redirect_uri_separator": { + "type": "string" + }, + "oauth_refresh_token_expire": { + "type": "integer" + }, + "oauth_token_expire": { + "type": "integer" + }, + "oauth_token_expired_retain_period": { + "type": "integer" + }, + "optimisations_use_async_session_write": { + "type": "boolean" + }, + "session_update_pool_size":{ + "type": "integer" + }, + "session_update_buffer_size":{ + "type": "integer" + }, + "pid_file_location": { + "type": "string" + }, + "policies": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "allow_explicit_policy_id": { + "type": "boolean" + }, + "policy_connection_string": { + "type": "string" + }, + "policy_record_name": { + "type": "string" + }, + "policy_source": { + "type": "string", + "enum": ["", "service", "rpc"] + } + } + }, + "proxy_default_timeout": { + "type": "integer" + }, + "proxy_ssl_insecure_skip_verify": { + "type": "boolean" + }, + "proxy_ssl_min_version": { + "type": "integer" + }, + "proxy_ssl_ciphers": { + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "public_key_path": { + "type": "string", + "format": "path" + }, + "reload_wait_time": { + "type": "integer" + }, + "secret": { + "type": "string" + }, + "sentry_code": { + "type": "string" + }, + "service_discovery": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "default_cache_timeout": { + "type": "integer" + } + } + }, + "slave_options": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "api_key": { + "type": "string" + }, + "bind_to_slugs": { + "type": "boolean" + }, + "call_timeout": { + "type": "integer" + }, + "connection_string": { + "type": "string" + }, + "disable_keyspace_sync": { + "type": "boolean" + }, + "enable_rpc_cache": { + "type": "boolean" + }, + "group_id": { + "type": "string" + }, + "ping_timeout": { + "type": "integer" + }, + "rpc_key": { + "type": "string" + }, + "ssl_insecure_skip_verify": { + "type": "boolean" + }, + "use_rpc": { + "type": "boolean" + }, + "use_ssl": { + "type": "boolean" + }, + "rpc_pool_size": { + "type": "integer" + } + } + }, + "statsd_connection_string": { + "type": "string" + }, + "statsd_prefix": { + "type": "string" + }, + "storage": { + "$ref": "#/definitions/StorageOptions" + }, + "suppress_default_org_store": { + "type": "boolean" + }, + "suppress_redis_signal_reload": { + "type": "boolean" + }, + "syslog_network_addr": { + "type": "string" + }, + "syslog_transport": { + "type": "string" + }, + "template_path": { + "type": "string", + "format": "path" + }, + "tyk_js_path": { + "type": "string", + "format": "path" + }, + "uptime_tests": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "config": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "checker_pool_size": { + "type": "integer" + }, + "enable_uptime_analytics": { + "type": "boolean" + }, + "failure_trigger_sample_size": { + "type": "integer" + }, + "time_wait": { + "type": "integer" + } + } + }, + "disable": { + "type": "boolean" + } + } + }, + "use_db_app_configs": { + "type": "boolean" + }, + "use_graylog": { + "type": "boolean" + }, + "use_logstash": { + "type": "boolean" + }, + "use_redis_log": { + "type": "boolean" + }, + "use_sentry": { + "type": "boolean" + }, + "use_syslog": { + "type": "boolean" + }, + "security": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "private_certificate_encoding_secret": { + "type": "string" + }, + "control_api_use_mutual_tls": { + "type": "boolean" + }, + "pinned_public_keys": { + "type": ["array", "null"], + "items": { + "type": "object" + } + }, + "certificates": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "upstream": { + "type": ["object", "null"] + }, + "apis": { + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "control_api": { + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "dashboard_api": { + "type": ["array", "null"], + "items": { + "type": "string" + } + }, + "mdcb_api": { + "type": ["array", "null"], + "items": { + "type": "string" + } + } + } + } + } + }, + "enable_key_logging": { + "type": "boolean" + }, + "newrelic": { + "type": ["object", "null"], + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "license_key": { + "type": "string" + } + } + }, + "enable_hashed_keys_listing": { + "type": "boolean" + }, + "min_token_length": { + "type": "integer" + }, + "disable_regexp_cache": { + "type": "boolean" + }, + "regexp_cache_expire": { + "type": "integer" + }, + "proxy_ssl_disable_renegotiation": { + "type": "boolean" + }, + "health_check_endpoint_name": { + "type": "string" + } +} +}` diff --git a/jsvm_goja.go b/jsvm_goja.go index 85abd7542e6..f528d90d211 100644 --- a/jsvm_goja.go +++ b/jsvm_goja.go @@ -12,10 +12,11 @@ import ( "time" "github.com/Sirupsen/logrus" + "github.com/dop251/goja" + "github.com/TykTechnologies/tyk/apidef" "github.com/TykTechnologies/tyk/config" "github.com/TykTechnologies/tyk/user" - "github.com/dop251/goja" _ "github.com/robertkrimen/otto/underscore" ) @@ -166,7 +167,6 @@ func (j *GojaJSVM) RunJSRequestDynamic(d *DynamicMiddleware, logger *logrus.Entr func (j *GojaJSVM) RunJSRequestVirtual(d *VirtualEndpoint, logger *logrus.Entry, vmeta *apidef.VirtualMeta, requestAsJson string, sessionAsJson string, specAsJson string) (error, int, string) { - interrupt := make(chan func(), 1) d.Logger().Debug("Running: ", vmeta.ResponseFunctionName) // buffered, leaving no chance of a goroutine leak since the // spawned goroutine will send 0 or 1 values. @@ -192,14 +192,12 @@ func (j *GojaJSVM) RunJSRequestVirtual(d *VirtualEndpoint, logger *logrus.Entry, return nil, -1, "" } t.Stop() + case <-t.C: t.Stop() d.Logger().Error("JS middleware timed out after ", j.GetTimeout()) - interrupt <- func() { - // only way to stop the VM is to send it a func - // that panics. - panic("stop") - } + + j.VM.Interrupt("Stopping VM for timeout") return nil, -1, "" } returnDataStr := returnRaw.String() @@ -394,7 +392,7 @@ func (j *GojaJSVM) LoadTykJSApi() { return nil } - doAddOrUpdate(apiKey, &newSession, suppressReset == "1") + doAddOrUpdate(apiKey, &newSession, suppressReset == "1", false) return nil }) diff --git a/jsvm_otto.go b/jsvm_otto.go index bf6fbb99c16..4029b652ce3 100644 --- a/jsvm_otto.go +++ b/jsvm_otto.go @@ -12,11 +12,12 @@ import ( "strings" "time" + "github.com/robertkrimen/otto" + _ "github.com/robertkrimen/otto/underscore" + "github.com/TykTechnologies/tyk/apidef" "github.com/TykTechnologies/tyk/config" "github.com/TykTechnologies/tyk/user" - "github.com/robertkrimen/otto" - _ "github.com/robertkrimen/otto/underscore" "github.com/Sirupsen/logrus" ) diff --git a/mw_js_plugin.go b/mw_js_plugin.go index a33d16a283e..94f8c0eaa03 100644 --- a/mw_js_plugin.go +++ b/mw_js_plugin.go @@ -144,9 +144,10 @@ func (d *DynamicMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Reques err, code, returnDataStr := d.Spec.JSVM.RunJSRequestDynamic(d, logger, string(requestAsJson), string(sessionAsJson), specAsJson) if err != nil { log.Errorf("JSVM error: %v", err) + return err, code } - if code != -1 { + if code != -1 || returnDataStr == "" { return nil, code } // Decode the return object diff --git a/mw_virtual_endpoint.go b/mw_virtual_endpoint.go index 6eb3c03f4b5..0a718420188 100644 --- a/mw_virtual_endpoint.go +++ b/mw_virtual_endpoint.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "encoding/json" "errors" - "fmt" "io" "io/ioutil" "net/http" @@ -178,8 +177,8 @@ func (d *VirtualEndpoint) ServeHTTPForCache(w http.ResponseWriter, r *http.Reque // Run the middleware err, code, returnDataStr := d.Spec.JSVM.RunJSRequestVirtual(d, d.logger, vmeta, string(requestAsJson), string(sessionAsJson), specAsJson) if err != nil { - //TODO - fmt.Println(err) + log.Errorf("JSVM VE error: %v", err) + return nil } if code != -1 { return nil diff --git a/mw_virtual_endpoint_test.go b/mw_virtual_endpoint_test.go index 44ec0a975bf..7bb33ef102b 100644 --- a/mw_virtual_endpoint_test.go +++ b/mw_virtual_endpoint_test.go @@ -64,7 +64,7 @@ func TestVirtualEndpointOtto(t *testing.T) { ts := newTykTestServer() defer ts.Close() - testPrepareVirtualEndpoint(virtTestJS, "GET") + testPrepareVirtualEndpoint(virtTestJS, "GET", "/virt", true) ts.Run(t, test.TestCase{ Path: "/virt", diff --git a/vendor/github.com/robertkrimen/otto/README.markdown b/vendor/github.com/robertkrimen/otto/README.markdown index a1ae7d1ae60..40584d32ba9 100644 --- a/vendor/github.com/robertkrimen/otto/README.markdown +++ b/vendor/github.com/robertkrimen/otto/README.markdown @@ -101,7 +101,7 @@ result, _ = vm.Run(` sayHello(); // Hello, undefined result = twoPlus(2.0); // 4 -`) +`) ``` ### Parser diff --git a/vendor/github.com/robertkrimen/otto/builtin_array.go b/vendor/github.com/robertkrimen/otto/builtin_array.go index 557ffc02480..56dd95ab693 100644 --- a/vendor/github.com/robertkrimen/otto/builtin_array.go +++ b/vendor/github.com/robertkrimen/otto/builtin_array.go @@ -170,7 +170,10 @@ func builtinArray_splice(call FunctionCall) Value { length := int64(toUint32(thisObject.get("length"))) start := valueToRangeIndex(call.Argument(0), length, false) - deleteCount := valueToRangeIndex(call.Argument(1), int64(length)-start, true) + deleteCount := length - start + if arg, ok := call.getArgument(1); ok { + deleteCount = valueToRangeIndex(arg, length-start, true) + } valueArray := make([]Value, deleteCount) for index := int64(0); index < deleteCount; index++ { diff --git a/vendor/github.com/robertkrimen/otto/error.go b/vendor/github.com/robertkrimen/otto/error.go index 41a5c10e7c1..c8b5af446ef 100644 --- a/vendor/github.com/robertkrimen/otto/error.go +++ b/vendor/github.com/robertkrimen/otto/error.go @@ -56,6 +56,7 @@ type _frame struct { file *file.File offset int callee string + fn interface{} } var ( diff --git a/vendor/github.com/robertkrimen/otto/inline.pl b/vendor/github.com/robertkrimen/otto/inline.pl index c3620b4a2b1..e9029048954 100755 --- a/vendor/github.com/robertkrimen/otto/inline.pl +++ b/vendor/github.com/robertkrimen/otto/inline.pl @@ -31,11 +31,11 @@ package otto func _newContext(runtime *_runtime) { @{[ join "\n", $self->newContext() ]} -} +} func newConsoleObject(runtime *_runtime) *_object { @{[ join "\n", $self->newConsoleObject() ]} -} +} _END_ for (qw/int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 float32 float64/) { @@ -730,7 +730,7 @@ sub propertyOrder { sub globalObject { my $self = shift; my $name = shift; - + my $propertyMap = ""; if (@_) { $propertyMap = join "\n", $self->propertyMap(@_); @@ -754,7 +754,7 @@ sub globalFunction { my $self = shift; my $name = shift; my $length = shift; - + my $builtin = "builtin${name}"; my $builtinNew = "builtinNew${name}"; my $prototype = "runtime.global.${name}Prototype"; @@ -874,7 +874,7 @@ sub newFunction { property: @{[ join "\n", $self->propertyMap(@propertyMap) ]}, $propertyOrder value: @{[ $self->nativeFunctionOf($name, $func) ]}, -} +} _END_ ); @@ -896,7 +896,7 @@ sub newObject { extensible: true, property: $propertyMap, $propertyOrder, -} +} _END_ } @@ -922,7 +922,7 @@ sub newPrototypeObject { property: $propertyMap, $propertyOrder, $value -} +} _END_ } diff --git a/vendor/github.com/robertkrimen/otto/runtime.go b/vendor/github.com/robertkrimen/otto/runtime.go index 7d29ecca0df..9941278f64d 100644 --- a/vendor/github.com/robertkrimen/otto/runtime.go +++ b/vendor/github.com/robertkrimen/otto/runtime.go @@ -1,6 +1,7 @@ package otto import ( + "encoding" "errors" "fmt" "math" @@ -8,6 +9,7 @@ import ( "reflect" "runtime" "strconv" + "strings" "sync" "github.com/robertkrimen/otto/ast" @@ -296,7 +298,23 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu if v.kind == valueObject { if gso, ok := v._object().value.(*_goStructObject); ok { if gso.value.Type().AssignableTo(t) { - return gso.value + // please see TestDynamicFunctionReturningInterface for why this exists + if t.Kind() == reflect.Interface && gso.value.Type().ConvertibleTo(t) { + return gso.value.Convert(t) + } else { + return gso.value + } + } + } + + if gao, ok := v._object().value.(*_goArrayObject); ok { + if gao.value.Type().AssignableTo(t) { + // please see TestDynamicFunctionReturningInterface for why this exists + if t.Kind() == reflect.Interface && gao.value.Type().ConvertibleTo(t) { + return gao.value.Convert(t) + } else { + return gao.value + } } } } @@ -444,6 +462,66 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu return []reflect.Value{self.convertCallParameter(rv, t.Out(0))} }) } + case reflect.Struct: + if o := v._object(); o != nil && o.class == "Object" { + s := reflect.New(t) + + for _, k := range o.propertyOrder { + var f *reflect.StructField + + for i := 0; i < t.NumField(); i++ { + ff := t.Field(i) + + if j := ff.Tag.Get("json"); j != "" { + if j == "-" { + continue + } + + a := strings.Split(j, ",") + + if a[0] == k { + f = &ff + break + } + } + + if ff.Name == k { + f = &ff + break + } + + if strings.EqualFold(ff.Name, k) { + f = &ff + } + } + + if f == nil { + panic(self.panicTypeError("can't convert object; field %q was supplied but does not exist on target %v", k, t)) + } + + ss := s + + for _, i := range f.Index { + if ss.Kind() == reflect.Ptr { + if ss.IsNil() { + if !ss.CanSet() { + panic(self.panicTypeError("can't set embedded pointer to unexported struct: %v", ss.Type().Elem())) + } + + ss.Set(reflect.New(ss.Type().Elem())) + } + + ss = ss.Elem() + } + + ss = ss.Field(i) + } + + ss.Set(self.convertCallParameter(o.get(k), ss.Type())) + } + + return s.Elem() + } } if tk == reflect.String { @@ -464,6 +542,20 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu return reflect.ValueOf(v.String()) } + if v.kind == valueString { + var s encoding.TextUnmarshaler + + if reflect.PtrTo(t).Implements(reflect.TypeOf(&s).Elem()) { + r := reflect.New(t) + + if err := r.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(v.string())); err != nil { + panic(self.panicSyntaxError("can't convert to %s: %s", t.String(), err.Error())) + } + + return r.Elem() + } + } + s := "OTTO DOES NOT UNDERSTAND THIS TYPE" switch v.kind { case valueBoolean: diff --git a/vendor/github.com/robertkrimen/otto/type_function.go b/vendor/github.com/robertkrimen/otto/type_function.go index 5637dc60512..ad4b1f16f55 100644 --- a/vendor/github.com/robertkrimen/otto/type_function.go +++ b/vendor/github.com/robertkrimen/otto/type_function.go @@ -37,7 +37,7 @@ type _nativeFunctionObject struct { construct _constructFunction // [[Construct]] } -func (runtime *_runtime) newNativeFunctionObject(name, file string, line int, native _nativeFunction, length int) *_object { +func (runtime *_runtime) _newNativeFunctionObject(name, file string, line int, native _nativeFunction, length int) *_object { self := runtime.newClassObject("Function") self.value = _nativeFunctionObject{ name: name, @@ -46,10 +46,35 @@ func (runtime *_runtime) newNativeFunctionObject(name, file string, line int, na call: native, construct: defaultConstruct, } + self.defineProperty("name", toValue_string(name), 0000, false) self.defineProperty("length", toValue_int(length), 0000, false) return self } +func (runtime *_runtime) newNativeFunctionObject(name, file string, line int, native _nativeFunction, length int) *_object { + self := runtime._newNativeFunctionObject(name, file, line, native, length) + self.defineOwnProperty("caller", _property{ + value: _propertyGetSet{ + runtime._newNativeFunctionObject("get", "internal", 0, func(fc FunctionCall) Value { + for sc := runtime.scope; sc != nil; sc = sc.outer { + if sc.frame.fn == self { + if sc.outer == nil || sc.outer.frame.fn == nil { + return nullValue + } + + return runtime.toValue(sc.outer.frame.fn) + } + } + + return nullValue + }, 0), + &_nilGetSetObject, + }, + mode: 0000, + }, false) + return self +} + // =================== // // _bindFunctionObject // // =================== // @@ -72,6 +97,7 @@ func (runtime *_runtime) newBoundFunctionObject(target *_object, this Value, arg if length < 0 { length = 0 } + self.defineProperty("name", toValue_string("bound "+target.get("name").String()), 0000, false) self.defineProperty("length", toValue_int(length), 0000, false) self.defineProperty("caller", Value{}, 0000, false) // TODO Should throw a TypeError self.defineProperty("arguments", Value{}, 0000, false) // TODO Should throw a TypeError @@ -106,7 +132,27 @@ func (runtime *_runtime) newNodeFunctionObject(node *_nodeFunctionLiteral, stash node: node, stash: stash, } + self.defineProperty("name", toValue_string(node.name), 0000, false) self.defineProperty("length", toValue_int(len(node.parameterList)), 0000, false) + self.defineOwnProperty("caller", _property{ + value: _propertyGetSet{ + runtime.newNativeFunction("get", "internal", 0, func(fc FunctionCall) Value { + for sc := runtime.scope; sc != nil; sc = sc.outer { + if sc.frame.fn == self { + if sc.outer == nil || sc.outer.frame.fn == nil { + return nullValue + } + + return runtime.toValue(sc.outer.frame.fn) + } + } + + return nullValue + }), + &_nilGetSetObject, + }, + mode: 0000, + }, false) return self } @@ -145,6 +191,7 @@ func (self *_object) call(this Value, argumentList []Value, eval bool, frame _fr nativeLine: fn.line, callee: fn.name, file: nil, + fn: self, } defer func() { rt.leaveScope() @@ -171,6 +218,7 @@ func (self *_object) call(this Value, argumentList []Value, eval bool, frame _fr rt.scope.frame = _frame{ callee: fn.node.name, file: fn.node.file, + fn: self, } defer func() { rt.leaveScope() diff --git a/vendor/vendor.json b/vendor/vendor.json index c233be122fb..bfd83d71e6a 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -485,10 +485,10 @@ "revisionTime": "2016-01-27T17:00:04Z" }, { - "checksumSHA1": "ZSYig6eYkc34ELHl6O5gRtJvr6o=", + "checksumSHA1": "WdorbyEJiNbjIOiQzM4TLb/XA7Q=", "path": "github.com/robertkrimen/otto", - "revision": "fc2eb1bbf1c5c462313c810bb02dc93cd964aebe", - "revisionTime": "2017-07-21T19:43:36Z" + "revision": "15f95af6e78dcd2030d8195a138bd88d4f403546", + "revisionTime": "2018-06-17T13:11:54Z" }, { "checksumSHA1": "q1HKpLIWi3tTT5W5LqMlwPcGmyQ=", From a73afc8f23cc81b0e25d3163ffbe21ac9ba2658a Mon Sep 17 00:00:00 2001 From: joshblakeley Date: Tue, 7 May 2019 09:52:04 +0100 Subject: [PATCH 7/8] rebase onto master and bring up to date --- cli/lint/schema.go | 766 ------------------------------------------- coprocess_test.go | 2 - jsvm_otto.go | 2 +- mw_js_plugin_test.go | 2 +- 4 files changed, 2 insertions(+), 770 deletions(-) delete mode 100644 cli/lint/schema.go diff --git a/cli/lint/schema.go b/cli/lint/schema.go deleted file mode 100644 index e881344bb0f..00000000000 --- a/cli/lint/schema.go +++ /dev/null @@ -1,766 +0,0 @@ -package lint - -const confSchema = `{ -"$schema": "http://json-schema.org/draft-04/schema#", -"type": "object", -"additionalProperties": false, -"definitions": { - "StorageOptions": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "database": { - "type": "integer" - }, - "enable_cluster": { - "type": "boolean" - }, - "use_ssl":{ - "type": "boolean" - }, - "ssl_insecure_skip_verify":{ - "type": "boolean" - }, - "host": { - "type": "string", - "format": "host-no-port" - }, - "hosts": { - "type": ["array", "null"] - }, - "optimisation_max_active": { - "type": "integer" - }, - "optimisation_max_idle": { - "type": "integer" - }, - "password": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "type": { - "type": "string", - "enum": ["", "redis"] - }, - "username": { - "type": "string" - } - } - } -}, -"properties": { - "allow_insecure_configs": { - "type": "boolean" - }, - "allow_master_keys": { - "type": "boolean" - }, - "allow_remote_config": { - "type": "boolean" - }, - "analytics_config": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "enable_detailed_recording": { - "type": "boolean" - }, - "enable_geo_ip": { - "type": "boolean" - }, - "geo_ip_db_path": { - "type": "string", - "format": "path" - }, - "ignored_ips": { - "type": ["array", "null"] - }, - "normalise_urls": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "custom_patterns": { - "type": ["array", "null"] - }, - "enabled": { - "type": "boolean" - }, - "normalise_numbers": { - "type": "boolean" - }, - "normalise_uuids": { - "type": "boolean" - } - } - }, - "pool_size": { - "type": "integer" - }, - "records_buffer_size": { - "type": "integer" - }, - "storage_expiration_time": { - "type": "integer" - }, - "type": { - "type": "string" - } - } - }, - "app_path": { - "type": "string", - "format": "path" - }, - "auth_override": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "auth_provider": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "meta": { - "type": ["array", "null"] - }, - "name": { - "type": "string" - }, - "storage_engine": { - "type": "string" - } - } - }, - "force_auth_provider": { - "type": "boolean" - }, - "force_session_provider": { - "type": "boolean" - }, - "session_provider": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "meta": { - "type": ["array", "null"] - }, - "name": { - "type": "string" - }, - "storage_engine": { - "type": "string" - } - } - } - } - }, - "bundle_base_url": { - "type": "string" - }, - "cache_storage": { - "$ref": "#/definitions/StorageOptions" - }, - "close_connections": { - "type": "boolean" - }, - "proxy_close_connections": { - "type": "boolean" - }, - "close_idle_connections": { - "type": "boolean" - }, - "control_api_hostname": { - "type": "string" - }, - "control_api_port": { - "type": "integer" - }, - "coprocess_options": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "coprocess_grpc_server": { - "type": "string" - }, - "enable_coprocess": { - "type": "boolean" - }, - "python_path_prefix": { - "type": "string" - } - } - }, - "db_app_conf_options": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "connection_string": { - "type": "string" - }, - "node_is_segmented": { - "type": "boolean" - }, - "tags": { - "type": ["array", "null"], - "items": { - "type": "string" - } - } - } - }, - "version_header":{ - "type": "string" - }, - "disable_dashboard_zeroconf": { - "type": "boolean" - }, - "disable_virtual_path_blobs": { - "type": "boolean" - }, - "drl_notification_frequency": { - "type": "integer" - }, - "enable_analytics": { - "type": "boolean" - }, - "enable_api_segregation": { - "type": "boolean" - }, - "enable_bundle_downloader": { - "type": "boolean" - }, - "enable_custom_domains": { - "type": "boolean" - }, - "enable_jsvm": { - "type": "boolean" - }, - "jsvm":{ - "type": "string" - }, - "jsvm_timeout": { - "type": "integer" - }, - "enable_non_transactional_rate_limiter": { - "type": "boolean" - }, - "enable_redis_rolling_limiter": { - "type": "boolean" - }, - "enable_sentinel_rate_limiter": { - "type": "boolean" - }, - "enable_separate_cache_store": { - "type": "boolean" - }, - "enforce_org_data_age": { - "type": "boolean" - }, - "enforce_org_data_detail_logging": { - "type": "boolean" - }, - "enforce_org_quotas": { - "type": "boolean" - }, - "event_handlers": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "events": { - "type": ["object", "null"], - "additionalProperties": false - } - } - }, - "event_trigers_defunct": { - "type": ["array", "null"] - }, - "experimental_process_org_off_thread": { - "type": "boolean" - }, - "force_global_session_lifetime": { - "type": "boolean" - }, - "global_session_lifetime": { - "type": "integer" - }, - "graylog_network_addr": { - "type": "string" - }, - "hash_keys": { - "type": "boolean" - }, - "hash_key_function": { - "type": "string", - "enum": ["", "murmur32", "murmur64", "murmur128", "sha256"] - }, - "health_check": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "enable_health_checks": { - "type": "boolean" - }, - "health_check_value_timeouts": { - "type": "integer" - } - } - }, - "hide_generator_header": { - "type": "boolean" - }, - "hostname": { - "type": "string" - }, - "http_server_options": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "certificates": { - "type": ["array", "null"], - "items": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "domain_name": { - "type": "string" - }, - "cert_file": { - "type": "string" - }, - "key_file": { - "type": "string" - } - } - } - }, - "enable_websockets": { - "type": "boolean" - }, - "flush_interval": { - "type": "integer" - }, - "min_version": { - "type": "integer" - }, - "override_defaults": { - "type": "boolean" - }, - "read_timeout": { - "type": "integer" - }, - "server_name": { - "type": "string" - }, - "skip_url_cleaning": { - "type": "boolean" - }, - "skip_target_path_escaping": { - "type": "boolean" - }, - "ssl_insecure_skip_verify": { - "type": "boolean" - }, - "use_ssl": { - "type": "boolean" - }, - "use_ssl_le": { - "type": "boolean" - }, - "write_timeout": { - "type": "integer" - }, - "ssl_certificates": { - "type": ["array", "null"], - "items": { - "type": "string" - } - }, - "ssl_ciphers":{ - "type": ["array", "null"], - "items": { - "type": "string" - } - } - } - }, - "legacy_enable_allowance_countdown": { - "type": "boolean" - }, - "listen_address": { - "type": "string", - "format": "host-no-port" - }, - "listen_port": { - "type": "integer" - }, - "local_session_cache": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "cached_session_eviction": { - "type": "integer" - }, - "cached_session_timeout": { - "type": "integer" - }, - "disable_cached_session_state": { - "type": "boolean" - } - } - }, - "log_level": { - "type": "string", - "enum": ["", "debug", "info", "warn", "error"] - }, - "logstash_network_addr": { - "type": "string" - }, - "logstash_transport": { - "type": "string" - }, - "management_node": { - "type": "boolean" - }, - "max_idle_connections_per_host": { - "type": "integer" - }, - "max_idle_connections": { - "type": "integer" - }, - "max_conn_time": { - "type": "integer" - }, - "middleware_path": { - "type": "string", - "format": "path" - }, - "monitor": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "configuration": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "event_timeout": { - "type": "integer" - }, - "header_map": { - "type": ["array", "null"] - }, - "method": { - "type": "string" - }, - "target_path": { - "type": "string" - }, - "template_path": { - "type": "string", - "format": "path" - } - } - }, - "enable_trigger_monitors": { - "type": "boolean" - }, - "global_trigger_limit": { - "type": "integer" - }, - "monitor_org_keys": { - "type": "boolean" - }, - "monitor_user_keys": { - "type": "boolean" - } - } - }, - "node_secret": { - "type": "string" - }, - "oauth_redirect_uri_separator": { - "type": "string" - }, - "oauth_refresh_token_expire": { - "type": "integer" - }, - "oauth_token_expire": { - "type": "integer" - }, - "oauth_token_expired_retain_period": { - "type": "integer" - }, - "optimisations_use_async_session_write": { - "type": "boolean" - }, - "session_update_pool_size":{ - "type": "integer" - }, - "session_update_buffer_size":{ - "type": "integer" - }, - "pid_file_location": { - "type": "string" - }, - "policies": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "allow_explicit_policy_id": { - "type": "boolean" - }, - "policy_connection_string": { - "type": "string" - }, - "policy_record_name": { - "type": "string" - }, - "policy_source": { - "type": "string", - "enum": ["", "service", "rpc"] - } - } - }, - "proxy_default_timeout": { - "type": "integer" - }, - "proxy_ssl_insecure_skip_verify": { - "type": "boolean" - }, - "proxy_ssl_min_version": { - "type": "integer" - }, - "proxy_ssl_ciphers": { - "type": ["array", "null"], - "items": { - "type": "string" - } - }, - "public_key_path": { - "type": "string", - "format": "path" - }, - "reload_wait_time": { - "type": "integer" - }, - "secret": { - "type": "string" - }, - "sentry_code": { - "type": "string" - }, - "service_discovery": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "default_cache_timeout": { - "type": "integer" - } - } - }, - "slave_options": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "api_key": { - "type": "string" - }, - "bind_to_slugs": { - "type": "boolean" - }, - "call_timeout": { - "type": "integer" - }, - "connection_string": { - "type": "string" - }, - "disable_keyspace_sync": { - "type": "boolean" - }, - "enable_rpc_cache": { - "type": "boolean" - }, - "group_id": { - "type": "string" - }, - "ping_timeout": { - "type": "integer" - }, - "rpc_key": { - "type": "string" - }, - "ssl_insecure_skip_verify": { - "type": "boolean" - }, - "use_rpc": { - "type": "boolean" - }, - "use_ssl": { - "type": "boolean" - }, - "rpc_pool_size": { - "type": "integer" - } - } - }, - "statsd_connection_string": { - "type": "string" - }, - "statsd_prefix": { - "type": "string" - }, - "storage": { - "$ref": "#/definitions/StorageOptions" - }, - "suppress_default_org_store": { - "type": "boolean" - }, - "suppress_redis_signal_reload": { - "type": "boolean" - }, - "syslog_network_addr": { - "type": "string" - }, - "syslog_transport": { - "type": "string" - }, - "template_path": { - "type": "string", - "format": "path" - }, - "tyk_js_path": { - "type": "string", - "format": "path" - }, - "uptime_tests": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "config": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "checker_pool_size": { - "type": "integer" - }, - "enable_uptime_analytics": { - "type": "boolean" - }, - "failure_trigger_sample_size": { - "type": "integer" - }, - "time_wait": { - "type": "integer" - } - } - }, - "disable": { - "type": "boolean" - } - } - }, - "use_db_app_configs": { - "type": "boolean" - }, - "use_graylog": { - "type": "boolean" - }, - "use_logstash": { - "type": "boolean" - }, - "use_redis_log": { - "type": "boolean" - }, - "use_sentry": { - "type": "boolean" - }, - "use_syslog": { - "type": "boolean" - }, - "security": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "private_certificate_encoding_secret": { - "type": "string" - }, - "control_api_use_mutual_tls": { - "type": "boolean" - }, - "pinned_public_keys": { - "type": ["array", "null"], - "items": { - "type": "object" - } - }, - "certificates": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "upstream": { - "type": ["object", "null"] - }, - "apis": { - "type": ["array", "null"], - "items": { - "type": "string" - } - }, - "control_api": { - "type": ["array", "null"], - "items": { - "type": "string" - } - }, - "dashboard_api": { - "type": ["array", "null"], - "items": { - "type": "string" - } - }, - "mdcb_api": { - "type": ["array", "null"], - "items": { - "type": "string" - } - } - } - } - } - }, - "enable_key_logging": { - "type": "boolean" - }, - "newrelic": { - "type": ["object", "null"], - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "license_key": { - "type": "string" - } - } - }, - "enable_hashed_keys_listing": { - "type": "boolean" - }, - "min_token_length": { - "type": "integer" - }, - "disable_regexp_cache": { - "type": "boolean" - }, - "regexp_cache_expire": { - "type": "integer" - }, - "proxy_ssl_disable_renegotiation": { - "type": "boolean" - }, - "health_check_endpoint_name": { - "type": "string" - } -} -}` diff --git a/coprocess_test.go b/coprocess_test.go index 32339e58d8a..e4cf487873c 100644 --- a/coprocess_test.go +++ b/coprocess_test.go @@ -15,8 +15,6 @@ import ( "github.com/golang/protobuf/proto" "github.com/justinas/alice" - "github.com/Sirupsen/logrus" - "github.com/TykTechnologies/tyk/apidef" "github.com/TykTechnologies/tyk/coprocess" ) diff --git a/jsvm_otto.go b/jsvm_otto.go index 4029b652ce3..cec3a6864f5 100644 --- a/jsvm_otto.go +++ b/jsvm_otto.go @@ -400,7 +400,7 @@ func (j *OttoJSVM) LoadTykJSApi() { return otto.Value{} } - doAddOrUpdate(apiKey, &newSession, suppressReset == "1") + doAddOrUpdate(apiKey, &newSession, suppressReset == "1", false) return otto.Value{} }) diff --git a/mw_js_plugin_test.go b/mw_js_plugin_test.go index ae2ccf764f7..447b4e66235 100644 --- a/mw_js_plugin_test.go +++ b/mw_js_plugin_test.go @@ -613,7 +613,7 @@ func TestJSVMRequestSchemeOtto(t *testing.T) { Pre: true, } req := httptest.NewRequest("GET", "/foo", nil) - jsvm := JSVM{} + jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) const js = ` From dd07c8024c255200f9e7ed39d466cdcc73bc620e Mon Sep 17 00:00:00 2001 From: joshblakeley Date: Tue, 7 May 2019 10:42:05 +0100 Subject: [PATCH 8/8] change config value to bool and reflect in package and tests --- config/config.go | 2 +- jsvm_goja.go | 6 ++---- main.go | 6 ++---- mw_js_plugin_test.go | 3 +-- mw_virtual_endpoint_test.go | 26 +++++++++++++++++++++++++- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/config/config.go b/config/config.go index 5f9a479a138..a4950e3e537 100644 --- a/config/config.go +++ b/config/config.go @@ -281,7 +281,7 @@ type Config struct { ControlAPIPort int `json:"control_api_port"` EnableCustomDomains bool `json:"enable_custom_domains"` EnableJSVM bool `json:"enable_jsvm"` - JSVM string `json:"jsvm"` + EnableV2JSVM bool `json:"enable_v2_jsvm"` JSVMTimeout int `json:"jsvm_timeout"` CoProcessOptions CoProcessConfig `json:"coprocess_options"` HideGeneratorHeader bool `json:"hide_generator_header"` diff --git a/jsvm_goja.go b/jsvm_goja.go index f528d90d211..93b4cbe5191 100644 --- a/jsvm_goja.go +++ b/jsvm_goja.go @@ -33,11 +33,9 @@ type TykJSVM interface { } func InitJSVM() TykJSVM { - - switch config.Global().JSVM { - case "goja": + if config.Global().EnableV2JSVM { return &GojaJSVM{} - default: + } else { return &OttoJSVM{} } } diff --git a/main.go b/main.go index 90078dba54e..d0381ecc1b2 100644 --- a/main.go +++ b/main.go @@ -483,11 +483,9 @@ func loadCustomMiddleware(spec *APISpec) ([]string, apidef.MiddlewareDefinition, mwPreFuncs := []apidef.MiddlewareDefinition{} mwPostFuncs := []apidef.MiddlewareDefinition{} mwPostKeyAuthFuncs := []apidef.MiddlewareDefinition{} - var mwDriver apidef.MiddlewareDriver - if config.Global().JSVM == "goja" { + mwDriver := apidef.OttoDriver + if config.Global().EnableV2JSVM { mwDriver = apidef.GojaDriver - } else { - mwDriver = apidef.OttoDriver } // Set AuthCheck hook if spec.CustomMiddleware.AuthCheck.Name != "" { diff --git a/mw_js_plugin_test.go b/mw_js_plugin_test.go index 447b4e66235..409a45eafa4 100644 --- a/mw_js_plugin_test.go +++ b/mw_js_plugin_test.go @@ -685,7 +685,6 @@ leakMid.NewProcessRequest(function(request, session) { func TestTykMakeHTTPRequestOtto(t *testing.T) { globalConf := config.Global() - globalConf.JSVM = "otto" config.SetGlobal(globalConf) ts := newTykTestServer() defer ts.Close() @@ -800,7 +799,7 @@ func TestTykMakeHTTPRequestOtto(t *testing.T) { func TestTykMakeHTTPRequestGoja(t *testing.T) { globalConf := config.Global() - globalConf.JSVM = "goja" + globalConf.EnableV2JSVM = true config.SetGlobal(globalConf) ts := newTykTestServer() defer ts.Close() diff --git a/mw_virtual_endpoint_test.go b/mw_virtual_endpoint_test.go index 7bb33ef102b..f8d7c42388d 100644 --- a/mw_virtual_endpoint_test.go +++ b/mw_virtual_endpoint_test.go @@ -79,7 +79,7 @@ func TestVirtualEndpointOtto(t *testing.T) { func TestVirtualEndpointGoja(t *testing.T) { globalConf := config.Global() - globalConf.JSVM = "goja" + globalConf.EnableV2JSVM = true config.SetGlobal(globalConf) defer resetTestConfig() ts := newTykTestServer() @@ -130,3 +130,27 @@ func BenchmarkVirtualEndpoint(b *testing.B) { }) } } + +func BenchmarkVirtualEndpointGoja(b *testing.B) { + b.ReportAllocs() + globalConf := config.Global() + globalConf.EnableV2JSVM = true + config.SetGlobal(globalConf) + defer resetTestConfig() + ts := newTykTestServer() + defer ts.Close() + + testPrepareVirtualEndpoint(virtTestJS, "GET", "/virt", true) + + for i := 0; i < b.N; i++ { + ts.Run(b, test.TestCase{ + Path: "/virt", + Code: 202, + BodyMatch: "foobar", + HeadersMatch: map[string]string{ + "data-foo": "x", + "data-bar-y": "3", + }, + }) + } +}