diff --git a/api_definition.go b/api_definition.go index 3c93ca6964b0..8cd532166309 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/api_loader.go b/api_loader.go index 70c036dd3ad5..03c078545be4 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 44f663120309..63e06fcc63e1 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/config/config.go b/config/config.go index 3743d6e79406..a4950e3e5377 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"` + 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/coprocess_bundle.go b/coprocess_bundle.go index a6d4539568d6..f1f1f61d691d 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 53cdcef3f2d5..e4cf487873c5 100644 --- a/coprocess_test.go +++ b/coprocess_test.go @@ -11,11 +11,10 @@ import ( "net/url" "testing" + "github.com/Sirupsen/logrus" "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_event_handler.go b/jsvm_event_handler.go index 53015832963d..c9a0371a03cd 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 000000000000..93b4cbe51917 --- /dev/null +++ b/jsvm_goja.go @@ -0,0 +1,432 @@ +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/dop251/goja" + + "github.com/TykTechnologies/tyk/apidef" + "github.com/TykTechnologies/tyk/config" + "github.com/TykTechnologies/tyk/user" + _ "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 { + if config.Global().EnableV2JSVM { + return &GojaJSVM{} + } else { + return &OttoJSVM{} + } +} + +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(` + requestAsJson + `, ` + 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) { + + 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()) + + j.VM.Interrupt("Stopping VM for timeout") + 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", false) + + 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.VM.RunString(`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) +} + +// 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 new file mode 100644 index 000000000000..cec3a6864f54 --- /dev/null +++ b/jsvm_otto.go @@ -0,0 +1,441 @@ +package main + +import ( + "crypto/tls" + "encoding/base64" + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "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/Sirupsen/logrus" +) + +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() + 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", 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.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) +} + +// 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() +} diff --git a/main.go b/main.go index 36eb72d25692..d0381ecc1b2e 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 @@ -484,12 +484,14 @@ func loadCustomMiddleware(spec *APISpec) ([]string, apidef.MiddlewareDefinition, mwPostFuncs := []apidef.MiddlewareDefinition{} mwPostKeyAuthFuncs := []apidef.MiddlewareDefinition{} mwDriver := apidef.OttoDriver - + if config.Global().EnableV2JSVM { + mwDriver = apidef.GojaDriver + } // 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 +555,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 a9cbdd4f74dc..94f8c0eaa035 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,16 @@ 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 + + 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 } - returnDataStr, _ := returnRaw.ToString() + if code != -1 || returnDataStr == "" { + return nil, code + } // Decode the return object newRequestData := VMReturnObject{} if err := json.Unmarshal([]byte(returnDataStr), &newRequestData); err != nil { @@ -285,318 +244,6 @@ 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: { diff --git a/mw_js_plugin_test.go b/mw_js_plugin_test.go index 6b0106cca4ff..409a45eafa46 100644 --- a/mw_js_plugin_test.go +++ b/mw_js_plugin_test.go @@ -21,13 +21,13 @@ 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 log.Formatter = new(prefixed.TextFormatter) - jsvm := JSVM{} + jsvm := &OttoJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) jsvm.RawLog = logrus.New() @@ -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{}}, @@ -81,7 +130,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 = ` @@ -91,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 @@ -108,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{}}, @@ -117,7 +207,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 @@ -131,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 @@ -148,7 +238,79 @@ 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", + } + 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 := &OttoJSVM{} + 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) + 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", @@ -157,6 +319,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, {}) });` @@ -165,9 +328,9 @@ testJSVMData.NewProcessRequest(function(request, session, spec) { MiddlewareClassName: "testJSVMData", Pre: true, } - jsvm := JSVM{} + jsvm := &GojaJSVM{} 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 @@ -179,7 +342,57 @@ 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", + } + 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 := &OttoJSVM{} + 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 + + 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", @@ -201,9 +414,9 @@ testJSVMData.NewProcessRequest(function(request, session, config) { MiddlewareClassName: "testJSVMData", Pre: true, } - jsvm := JSVM{} + jsvm := &GojaJSVM{} 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 @@ -229,7 +442,45 @@ 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", + } + 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 := &OttoJSVM{} + 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 TestJSVMReturnOverridesErrorGoja(t *testing.T) { spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} spec.ConfigData = map[string]interface{}{ "foo": "bar", @@ -247,9 +498,9 @@ testJSVMData.NewProcessRequest(function(request, session, config) { MiddlewareClassName: "testJSVMData", Pre: true, } - jsvm := JSVM{} + jsvm := &GojaJSVM{} 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 @@ -266,8 +517,51 @@ testJSVMData.NewProcessRequest(function(request, session, config) { 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({}) -func TestJSVMUserCore(t *testing.T) { +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 := &OttoJSVM{} + 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 TestJSVMUserCoreGoja(t *testing.T) { spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} const js = ` var testJSVMCore = new TykJS.TykMiddleware.NewMiddleware({}) @@ -296,9 +590,9 @@ testJSVMCore.NewProcessRequest(function(request, session, config) { globalConf.TykJSPath = old config.SetGlobal(globalConf) }() - jsvm := JSVM{} + jsvm := &GojaJSVM{} 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 @@ -310,8 +604,46 @@ testJSVMCore.NewProcessRequest(function(request, session, config) { 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{}}, + }, + MiddlewareClassName: "leakMid", + Pre: true, + } + req := httptest.NewRequest("GET", "/foo", nil) + jsvm := &OttoJSVM{} + 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 + 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 TestJSVMRequestScheme(t *testing.T) { +func TestJSVMRequestSchemeGoja(t *testing.T) { dynMid := &DynamicMiddleware{ BaseMiddleware: BaseMiddleware{ Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}}, @@ -320,7 +652,8 @@ func TestJSVMRequestScheme(t *testing.T) { Pre: true, } req := httptest.NewRequest("GET", "/foo", nil) - jsvm := JSVM{} + req.URL.Scheme = "http" + jsvm := &GojaJSVM{} jsvm.Init(nil, logrus.NewEntry(log)) const js = ` @@ -333,7 +666,7 @@ 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 @@ -350,7 +683,9 @@ leakMid.NewProcessRequest(function(request, session) { } } -func TestTykMakeHTTPRequest(t *testing.T) { +func TestTykMakeHTTPRequestOtto(t *testing.T) { + globalConf := config.Global() + config.SetGlobal(globalConf) ts := newTykTestServer() defer ts.Close() @@ -462,8 +797,139 @@ func TestTykMakeHTTPRequest(t *testing.T) { }) } -func TestJSVMBase64(t *testing.T) { - jsvm := JSVM{} +func TestTykMakeHTTPRequestGoja(t *testing.T) { + globalConf := config.Global() + globalConf.EnableV2JSVM = true + 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)) + + 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 := 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" @@ -472,52 +938,52 @@ 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 := 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) } }) diff --git a/mw_virtual_endpoint.go b/mw_virtual_endpoint.go index 4223f9c8f210..0a7184201884 100644 --- a/mw_virtual_endpoint.go +++ b/mw_virtual_endpoint.go @@ -9,13 +9,11 @@ import ( "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 +53,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 +174,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 { + log.Errorf("JSVM VE error: %v", err) + return nil + } + 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 cb4ebcfb25ae..f8d7c42388d9 100644 --- a/mw_virtual_endpoint_test.go +++ b/mw_virtual_endpoint_test.go @@ -6,11 +6,13 @@ import ( "testing" "github.com/TykTechnologies/tyk/apidef" + "github.com/TykTechnologies/tyk/config" "github.com/TykTechnologies/tyk/test" ) const virtTestJS = ` function testVirtData(request, session, config) { + var resp = { Body: "foobar", Headers: { @@ -19,6 +21,7 @@ function testVirtData(request, session, config) { }, Code: 202 } + return TykJsResponse(resp, session.meta_data) } ` @@ -57,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", "/virt", true) + + 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.EnableV2JSVM = true + config.SetGlobal(globalConf) + defer resetTestConfig() ts := newTykTestServer() defer ts.Close() @@ -106,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", + }, + }) + } +} diff --git a/vendor/github.com/robertkrimen/otto/README.markdown b/vendor/github.com/robertkrimen/otto/README.markdown index a1ae7d1ae60b..40584d32ba95 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 557ffc02480e..56dd95ab6934 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 41a5c10e7c12..c8b5af446ef1 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 c3620b4a2b16..e90290489541 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 7d29ecca0df5..9941278f64d0 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 5637dc60512b..ad4b1f16f55f 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 c233be122fbb..bfd83d71e6ac 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=",