diff --git a/api_loader.go b/api_loader.go index c863c0bc813..0331e9b4ee6 100644 --- a/api_loader.go +++ b/api_loader.go @@ -209,7 +209,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) } @@ -274,7 +274,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}) @@ -304,7 +304,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}) @@ -324,7 +324,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}) @@ -364,8 +364,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 @@ -375,7 +375,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") @@ -411,7 +411,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 4a54845ee61..f65922a4595 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 47c53fd090a..d8f58689fc6 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" @@ -24,6 +25,9 @@ var ( CoProcessName = apidef.MiddlewareDriver("test") MessageType = coprocess.ProtobufMessage testDispatcher, _ = NewCoProcessDispatcher() + coprocessLog = log.WithFields(logrus.Fields{ + "prefix": "coprocess", + }) ) /* Dispatcher functions */ @@ -35,17 +39,15 @@ func TestCoProcessDispatch(t *testing.T) { } messagePtr := testDispatcher.ToCoProcessMessage(object) - newMessagePtr := testDispatcher.Dispatch(messagePtr) - - newObject := testDispatcher.ToCoProcessObject(newMessagePtr) - t.Log(newObject) + newMessagePtr := testDispatcher.ToCoProcessMessage(&coprocess.Object{}) + testDispatcher.Dispatch(messagePtr, newMessagePtr) } func TestCoProcessDispatchEvent(t *testing.T) { spec := createSpecTest(t, basicCoProcessDef) remote, _ := url.Parse(spec.Proxy.TargetURL) proxy := TykNewSingleHostReverseProxy(remote, spec) - baseMid := BaseMiddleware{spec, proxy} + baseMid := BaseMiddleware{spec, proxy, coprocessLog} meta := EventKeyFailureMeta{ EventMetaDefault: EventMetaDefault{Message: "Auth Failure"}, @@ -127,7 +129,7 @@ func buildCoProcessChain(spec *APISpec, hookName string, hookType coprocess.Hook remote, _ := url.Parse(spec.Proxy.TargetURL) proxy := TykNewSingleHostReverseProxy(remote, spec) proxyHandler := ProxyHandler(proxy, spec) - baseMid := BaseMiddleware{spec, proxy} + baseMid := BaseMiddleware{spec, proxy, coprocessLog} mw := CreateCoProcessMiddleware(hookName, hookType, driver, baseMid) return alice.New(mw).Then(proxyHandler) } diff --git a/coprocess_test_helpers.go b/coprocess_test_helpers.go index 12695282e8e..927486f6eb0 100644 --- a/coprocess_test_helpers.go +++ b/coprocess_test_helpers.go @@ -17,15 +17,11 @@ static int TestMessageLength(struct CoProcessMessage* object) { return object->length; } -static struct CoProcessMessage* TestDispatchHook(struct CoProcessMessage* object) { - struct CoProcessMessage* outputObject = malloc(sizeof *outputObject); - - outputObject->p_data = object->p_data; - outputObject->length = object->length; - - applyTestHooks(outputObject); - - return outputObject; +static void TestDispatchHook(struct CoProcessMessage* object, struct CoProcessMessage* newObject) { + struct CoProcessMessage* outputObject = malloc(sizeof *outputObject); newObject->p_data = object->p_data; + newObject->length = object->length; + outputObject->p_data = object->p_data; applyTestHooks(newObject); + outputObject->length = object->length; return; }; */ @@ -51,10 +47,11 @@ type TestDispatcher struct { /* Basic CoProcessDispatcher functions */ -func (d *TestDispatcher) Dispatch(objectPtr unsafe.Pointer) unsafe.Pointer { +func (d *TestDispatcher) Dispatch(objectPtr unsafe.Pointer, newObjectPtr unsafe.Pointer) error { object := (*C.struct_CoProcessMessage)(objectPtr) - newObjectPtr := C.TestDispatchHook(object) - return unsafe.Pointer(newObjectPtr) + newObject := (*C.struct_CoProcessMessage)(newObjectPtr) + C.TestDispatchHook(object, newObject) + return nil } func (d *TestDispatcher) DispatchEvent(eventJSON []byte) { 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 0fe69f0bb5a..e2578596aba 100644 --- a/main.go +++ b/main.go @@ -465,13 +465,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) } } @@ -535,7 +539,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 af943caf148..68dc0cffb8f 100644 --- a/mw_js_plugin.go +++ b/mw_js_plugin.go @@ -136,10 +136,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 } @@ -233,3 +234,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 ec42bbe2482..b19a2fe846f 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,7 @@ 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 @@ -180,7 +310,39 @@ testJSVMData.NewProcessRequest(function(request, session, spec) { } } -func TestJSVMReturnOverridesFullResponse(t *testing.T) { +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 + + 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 TestJSVMReturnOverridesFullResponseOtto(t *testing.T) { spec := &APISpec{APIDefinition: &apidef.APIDefinition{}} spec.ConfigData = map[string]interface{}{ "foo": "bar", @@ -204,7 +366,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 @@ -230,7 +392,57 @@ testJSVMData.NewProcessRequest(function(request, session, config) { } } -func TestJSVMReturnOverridesError(t *testing.T) { +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 + + 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 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{}}, @@ -335,7 +627,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 @@ -352,7 +684,8 @@ leakMid.NewProcessRequest(function(request, session) { } } -func TestTykMakeHTTPRequest(t *testing.T) { +func TestTykMakeHTTPRequestOtto(t *testing.T) { + ts := newTykTestServer() defer ts.Close() @@ -419,7 +752,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)) @@ -429,52 +832,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) } })