github.com/dbernstein1/tyk@v2.9.0-beta9-dl-apic+incompatible/gateway/mw_js_plugin_test.go (about)

     1  package gateway
     2  
     3  import (
     4  	"bytes"
     5  	"io"
     6  	"io/ioutil"
     7  	"net/http/httptest"
     8  	"regexp"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/sirupsen/logrus"
    14  	prefixed "github.com/x-cray/logrus-prefixed-formatter"
    15  
    16  	"github.com/TykTechnologies/tyk/apidef"
    17  	"github.com/TykTechnologies/tyk/config"
    18  	logger "github.com/TykTechnologies/tyk/log"
    19  	"github.com/TykTechnologies/tyk/test"
    20  )
    21  
    22  func TestJSVMLogs(t *testing.T) {
    23  	var buf bytes.Buffer
    24  	log := logrus.New()
    25  	log.Out = &buf
    26  	log.Formatter = new(prefixed.TextFormatter)
    27  
    28  	jsvm := JSVM{}
    29  	jsvm.Init(nil, logrus.NewEntry(log))
    30  
    31  	jsvm.RawLog = logrus.New()
    32  	jsvm.RawLog.Out = &buf
    33  	jsvm.RawLog.Formatter = new(logger.RawFormatter)
    34  
    35  	const in = `
    36  log("foo")
    37  log('{"x": "y"}')
    38  rawlog("foo")
    39  rawlog('{"x": "y"}')
    40  `
    41  
    42  	want := []string{
    43  		`time=TIME level=info msg=foo prefix=jsvm type=log-msg`,
    44  		`time=TIME level=info msg="{"x": "y"}" prefix=jsvm type=log-msg`,
    45  		`foo`,
    46  		`{"x": "y"}`,
    47  	}
    48  	if _, err := jsvm.VM.Run(in); err != nil {
    49  		t.Fatalf("failed to run js: %v", err)
    50  	}
    51  	got := strings.Split(strings.Trim(buf.String(), "\n"), "\n")
    52  	i := 0
    53  	timeRe := regexp.MustCompile(`time="[^"]*"`)
    54  	for _, line := range got {
    55  		if i >= len(want) {
    56  			t.Logf("too many lines")
    57  			t.Fail()
    58  			break
    59  		}
    60  		s := timeRe.ReplaceAllString(line, "time=TIME")
    61  		if s != line && !strings.Contains(s, "type=log-msg") {
    62  			continue // log line from elsewhere (async)
    63  		}
    64  		if s != want[i] {
    65  			t.Logf("%s != %s", s, want[i])
    66  			t.Fail()
    67  		}
    68  		i++
    69  	}
    70  }
    71  
    72  func TestJSVMBody(t *testing.T) {
    73  	dynMid := &DynamicMiddleware{
    74  		BaseMiddleware: BaseMiddleware{
    75  			Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}},
    76  		},
    77  		MiddlewareClassName: "leakMid",
    78  		Pre:                 true,
    79  	}
    80  	body := "foô \uffff \u0000 \xff bàr"
    81  	req := httptest.NewRequest("GET", "/foo", strings.NewReader(body))
    82  	jsvm := JSVM{}
    83  	jsvm.Init(nil, logrus.NewEntry(log))
    84  
    85  	const js = `
    86  var leakMid = new TykJS.TykMiddleware.NewMiddleware({})
    87  
    88  leakMid.NewProcessRequest(function(request, session) {
    89  	request.Body += " appended"
    90  	return leakMid.ReturnData(request, session.meta_data)
    91  });`
    92  	if _, err := jsvm.VM.Run(js); err != nil {
    93  		t.Fatalf("failed to set up js plugin: %v", err)
    94  	}
    95  	dynMid.Spec.JSVM = jsvm
    96  	dynMid.ProcessRequest(nil, req, nil)
    97  
    98  	bs, err := ioutil.ReadAll(req.Body)
    99  	if err != nil {
   100  		t.Fatalf("failed to read final body: %v", err)
   101  	}
   102  	want := body + " appended"
   103  	if got := string(bs); want != got {
   104  		t.Fatalf("JS plugin broke non-UTF8 body %q into %q",
   105  			want, got)
   106  	}
   107  }
   108  
   109  func TestJSVMProcessTimeout(t *testing.T) {
   110  	dynMid := &DynamicMiddleware{
   111  		BaseMiddleware: BaseMiddleware{
   112  			Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}},
   113  		},
   114  		MiddlewareClassName: "leakMid",
   115  		Pre:                 true,
   116  	}
   117  	req := httptest.NewRequest("GET", "/foo", strings.NewReader("body"))
   118  	jsvm := JSVM{}
   119  	jsvm.Init(nil, logrus.NewEntry(log))
   120  	jsvm.Timeout = time.Millisecond
   121  
   122  	// this js plugin just loops forever, keeping Otto at 100% CPU
   123  	// usage and running forever.
   124  	const js = `
   125  var leakMid = new TykJS.TykMiddleware.NewMiddleware({})
   126  
   127  leakMid.NewProcessRequest(function(request, session) {
   128  	while (true) {
   129  	}
   130  	return leakMid.ReturnData(request, session.meta_data)
   131  });`
   132  	if _, err := jsvm.VM.Run(js); err != nil {
   133  		t.Fatalf("failed to set up js plugin: %v", err)
   134  	}
   135  	dynMid.Spec.JSVM = jsvm
   136  
   137  	done := make(chan bool)
   138  	go func() {
   139  		dynMid.ProcessRequest(nil, req, nil)
   140  		done <- true
   141  	}()
   142  	select {
   143  	case <-done:
   144  	case <-time.After(time.Second):
   145  		t.Fatal("js vm wasn't killed after its timeout")
   146  	}
   147  }
   148  
   149  func TestJSVMConfigData(t *testing.T) {
   150  	spec := &APISpec{APIDefinition: &apidef.APIDefinition{}}
   151  	spec.ConfigData = map[string]interface{}{
   152  		"foo": "bar",
   153  	}
   154  	const js = `
   155  var testJSVMData = new TykJS.TykMiddleware.NewMiddleware({})
   156  
   157  testJSVMData.NewProcessRequest(function(request, session, spec) {
   158  	request.SetHeaders["data-foo"] = spec.config_data.foo
   159  	return testJSVMData.ReturnData(request, {})
   160  });`
   161  	dynMid := &DynamicMiddleware{
   162  		BaseMiddleware:      BaseMiddleware{Spec: spec, Proxy: nil},
   163  		MiddlewareClassName: "testJSVMData",
   164  		Pre:                 true,
   165  	}
   166  	jsvm := JSVM{}
   167  	jsvm.Init(nil, logrus.NewEntry(log))
   168  	if _, err := jsvm.VM.Run(js); err != nil {
   169  		t.Fatalf("failed to set up js plugin: %v", err)
   170  	}
   171  	dynMid.Spec.JSVM = jsvm
   172  
   173  	r := TestReq(t, "GET", "/v1/test-data", nil)
   174  	dynMid.ProcessRequest(nil, r, nil)
   175  	if want, got := "bar", r.Header.Get("data-foo"); want != got {
   176  		t.Fatalf("wanted header to be %q, got %q", want, got)
   177  	}
   178  }
   179  
   180  func TestJSVMReturnOverridesFullResponse(t *testing.T) {
   181  	spec := &APISpec{APIDefinition: &apidef.APIDefinition{}}
   182  	spec.ConfigData = map[string]interface{}{
   183  		"foo": "bar",
   184  	}
   185  	const js = `
   186  var testJSVMData = new TykJS.TykMiddleware.NewMiddleware({})
   187  
   188  testJSVMData.NewProcessRequest(function(request, session, config) {
   189  	request.ReturnOverrides.ResponseError = "Foobarbaz"
   190  	request.ReturnOverrides.ResponseCode = 200
   191  	request.ReturnOverrides.ResponseHeaders = {
   192  		"X-Foo": "Bar",
   193  		"X-Baz": "Qux"
   194  	}
   195  	return testJSVMData.ReturnData(request, {})
   196  });`
   197  	dynMid := &DynamicMiddleware{
   198  		BaseMiddleware:      BaseMiddleware{Spec: spec, Proxy: nil},
   199  		MiddlewareClassName: "testJSVMData",
   200  		Pre:                 true,
   201  	}
   202  	jsvm := JSVM{}
   203  	jsvm.Init(nil, logrus.NewEntry(log))
   204  	if _, err := jsvm.VM.Run(js); err != nil {
   205  		t.Fatalf("failed to set up js plugin: %v", err)
   206  	}
   207  	dynMid.Spec.JSVM = jsvm
   208  
   209  	rec := httptest.NewRecorder()
   210  	r := TestReq(t, "GET", "/v1/test-data", nil)
   211  	dynMid.ProcessRequest(rec, r, nil)
   212  
   213  	wantBody := "Foobarbaz"
   214  	gotBody := rec.Body.String()
   215  	if wantBody != gotBody {
   216  		t.Fatalf("wanted body to be %q, got %q", wantBody, gotBody)
   217  	}
   218  	if want, got := "Bar", rec.HeaderMap.Get("x-foo"); got != want {
   219  		t.Fatalf("wanted header to be %q, got %q", want, got)
   220  	}
   221  	if want, got := "Qux", rec.HeaderMap.Get("x-baz"); got != want {
   222  		t.Fatalf("wanted header to be %q, got %q", want, got)
   223  	}
   224  
   225  	if want := 200; rec.Code != 200 {
   226  		t.Fatalf("wanted code to be %d, got %d", want, rec.Code)
   227  	}
   228  }
   229  
   230  func TestJSVMReturnOverridesError(t *testing.T) {
   231  	spec := &APISpec{APIDefinition: &apidef.APIDefinition{}}
   232  	spec.ConfigData = map[string]interface{}{
   233  		"foo": "bar",
   234  	}
   235  	const js = `
   236  var testJSVMData = new TykJS.TykMiddleware.NewMiddleware({})
   237  
   238  testJSVMData.NewProcessRequest(function(request, session, config) {
   239  	request.ReturnOverrides.ResponseError = "Foobarbaz"
   240  	request.ReturnOverrides.ResponseCode = 401
   241  	return testJSVMData.ReturnData(request, {})
   242  });`
   243  	dynMid := &DynamicMiddleware{
   244  		BaseMiddleware:      BaseMiddleware{Spec: spec, Proxy: nil},
   245  		MiddlewareClassName: "testJSVMData",
   246  		Pre:                 true,
   247  	}
   248  	jsvm := JSVM{}
   249  	jsvm.Init(nil, logrus.NewEntry(log))
   250  	if _, err := jsvm.VM.Run(js); err != nil {
   251  		t.Fatalf("failed to set up js plugin: %v", err)
   252  	}
   253  	dynMid.Spec.JSVM = jsvm
   254  
   255  	r := TestReq(t, "GET", "/v1/test-data", nil)
   256  	err, code := dynMid.ProcessRequest(nil, r, nil)
   257  
   258  	if want := 401; code != 401 {
   259  		t.Fatalf("wanted code to be %d, got %d", want, code)
   260  	}
   261  
   262  	wantBody := "Foobarbaz"
   263  	if !strings.Contains(err.Error(), wantBody) {
   264  		t.Fatalf("wanted body to contain to be %v, got %v", wantBody, err.Error())
   265  	}
   266  }
   267  
   268  func TestJSVMUserCore(t *testing.T) {
   269  	spec := &APISpec{APIDefinition: &apidef.APIDefinition{}}
   270  	const js = `
   271  var testJSVMCore = new TykJS.TykMiddleware.NewMiddleware({})
   272  
   273  testJSVMCore.NewProcessRequest(function(request, session, config) {
   274  	request.SetHeaders["global"] = globalVar
   275  	return testJSVMCore.ReturnData(request, {})
   276  });`
   277  	dynMid := &DynamicMiddleware{
   278  		BaseMiddleware:      BaseMiddleware{Spec: spec, Proxy: nil},
   279  		MiddlewareClassName: "testJSVMCore",
   280  		Pre:                 true,
   281  	}
   282  	tfile, err := ioutil.TempFile("", "tykjs")
   283  	if err != nil {
   284  		t.Fatal(err)
   285  	}
   286  	if _, err := io.WriteString(tfile, `var globalVar = "globalValue"`); err != nil {
   287  		t.Fatal(err)
   288  	}
   289  	globalConf := config.Global()
   290  	old := globalConf.TykJSPath
   291  	globalConf.TykJSPath = tfile.Name()
   292  	config.SetGlobal(globalConf)
   293  	defer func() {
   294  		globalConf.TykJSPath = old
   295  		config.SetGlobal(globalConf)
   296  	}()
   297  	jsvm := JSVM{}
   298  	jsvm.Init(nil, logrus.NewEntry(log))
   299  	if _, err := jsvm.VM.Run(js); err != nil {
   300  		t.Fatalf("failed to set up js plugin: %v", err)
   301  	}
   302  	dynMid.Spec.JSVM = jsvm
   303  
   304  	r := TestReq(t, "GET", "/foo", nil)
   305  	dynMid.ProcessRequest(nil, r, nil)
   306  
   307  	if want, got := "globalValue", r.Header.Get("global"); want != got {
   308  		t.Fatalf("wanted header to be %q, got %q", want, got)
   309  	}
   310  }
   311  
   312  func TestJSVMRequestScheme(t *testing.T) {
   313  	dynMid := &DynamicMiddleware{
   314  		BaseMiddleware: BaseMiddleware{
   315  			Spec: &APISpec{APIDefinition: &apidef.APIDefinition{}},
   316  		},
   317  		MiddlewareClassName: "leakMid",
   318  		Pre:                 true,
   319  	}
   320  	req := httptest.NewRequest("GET", "/foo", nil)
   321  	jsvm := JSVM{}
   322  	jsvm.Init(nil, logrus.NewEntry(log))
   323  
   324  	const js = `
   325  var leakMid = new TykJS.TykMiddleware.NewMiddleware({})
   326  leakMid.NewProcessRequest(function(request, session) {
   327  	var test = request.Scheme += " appended"
   328  	var responseObject = {
   329          Body: test,
   330          Code: 200
   331      }
   332  	return leakMid.ReturnData(responseObject, session.meta_data)
   333  });`
   334  	if _, err := jsvm.VM.Run(js); err != nil {
   335  		t.Fatalf("failed to set up js plugin: %v", err)
   336  	}
   337  	dynMid.Spec.JSVM = jsvm
   338  	dynMid.ProcessRequest(nil, req, nil)
   339  
   340  	bs, err := ioutil.ReadAll(req.Body)
   341  	if err != nil {
   342  		t.Fatalf("failed to read final body: %v", err)
   343  	}
   344  	want := "http" + " appended"
   345  	if got := string(bs); want != got {
   346  		t.Fatalf("JS plugin broke non-UTF8 body %q into %q",
   347  			want, got)
   348  	}
   349  }
   350  
   351  func TestTykMakeHTTPRequest(t *testing.T) {
   352  	ts := StartTest()
   353  	defer ts.Close()
   354  
   355  	bundle := RegisterBundle("jsvm_make_http_request", map[string]string{
   356  		"manifest.json": `
   357  		{
   358  		    "file_list": [],
   359  		    "custom_middleware": {
   360  		        "driver": "otto",
   361  		        "pre": [{
   362  		            "name": "testTykMakeHTTPRequest",
   363  		            "path": "middleware.js"
   364  		        }]
   365  		    }
   366  		}
   367  	`,
   368  		"middleware.js": `
   369  	var testTykMakeHTTPRequest = new TykJS.TykMiddleware.NewMiddleware({})
   370  
   371  	testTykMakeHTTPRequest.NewProcessRequest(function(request, session, spec) {
   372  		var newRequest = {
   373  			"Method": "GET",
   374  			"Headers": {"Accept": "application/json"},
   375  			"Domain": spec.config_data.base_url,
   376  			"Resource": "/api/get?param1=dummy"
   377  		}
   378  
   379  		var resp = TykMakeHttpRequest(JSON.stringify(newRequest));
   380  		var usableResponse = JSON.parse(resp);
   381  
   382  		if(usableResponse.Code > 400) {
   383  			request.ReturnOverrides.ResponseCode = usableResponse.code
   384  			request.ReturnOverrides.ResponseError = "error"
   385  		}
   386  
   387  		request.Body = usableResponse.Body
   388  
   389  		return testTykMakeHTTPRequest.ReturnData(request, {})
   390  	});
   391  	`})
   392  
   393  	t.Run("Existing endpoint", func(t *testing.T) {
   394  		BuildAndLoadAPI(func(spec *APISpec) {
   395  			spec.Proxy.ListenPath = "/sample"
   396  			spec.ConfigData = map[string]interface{}{
   397  				"base_url": ts.URL,
   398  			}
   399  			spec.CustomMiddlewareBundle = bundle
   400  		}, func(spec *APISpec) {
   401  			spec.Proxy.ListenPath = "/api"
   402  		})
   403  
   404  		ts.Run(t, test.TestCase{Path: "/sample", Code: 200})
   405  	})
   406  
   407  	t.Run("Nonexistent endpoint", func(t *testing.T) {
   408  		BuildAndLoadAPI(func(spec *APISpec) {
   409  			spec.Proxy.ListenPath = "/sample"
   410  			spec.ConfigData = map[string]interface{}{
   411  				"base_url": ts.URL,
   412  			}
   413  			spec.CustomMiddlewareBundle = bundle
   414  		})
   415  
   416  		ts.Run(t, test.TestCase{Path: "/sample", Code: 404})
   417  	})
   418  
   419  	t.Run("Endpoint with query", func(t *testing.T) {
   420  		BuildAndLoadAPI(func(spec *APISpec) {
   421  			spec.Proxy.ListenPath = "/sample"
   422  			spec.ConfigData = map[string]interface{}{
   423  				"base_url": ts.URL,
   424  			}
   425  			spec.CustomMiddlewareBundle = bundle
   426  		}, func(spec *APISpec) {
   427  			spec.Proxy.ListenPath = "/api"
   428  		})
   429  
   430  		ts.Run(t, test.TestCase{Path: "/sample", BodyMatch: "/api/get?param1=dummy", Code: 200})
   431  	})
   432  
   433  	t.Run("Endpoint with skip cleaning", func(t *testing.T) {
   434  		ts.Close()
   435  		globalConf := config.Global()
   436  		globalConf.HttpServerOptions.SkipURLCleaning = true
   437  		globalConf.HttpServerOptions.OverrideDefaults = true
   438  		config.SetGlobal(globalConf)
   439  
   440  		prevSkipClean := defaultTestConfig.HttpServerOptions.OverrideDefaults &&
   441  			defaultTestConfig.HttpServerOptions.SkipURLCleaning
   442  		testServerRouter.SkipClean(true)
   443  		defer testServerRouter.SkipClean(prevSkipClean)
   444  
   445  		ts := StartTest()
   446  		defer ts.Close()
   447  		defer ResetTestConfig()
   448  
   449  		BuildAndLoadAPI(func(spec *APISpec) {
   450  			spec.Proxy.ListenPath = "/sample"
   451  			spec.ConfigData = map[string]interface{}{
   452  				"base_url": ts.URL,
   453  			}
   454  			spec.CustomMiddlewareBundle = bundle
   455  		}, func(spec *APISpec) {
   456  			spec.Proxy.ListenPath = "/api"
   457  		})
   458  
   459  		ts.Run(t, test.TestCase{Path: "/sample/99999-XXXX+%2F%2F+dog+9+fff%C3%A9o+party", BodyMatch: "URI\":\"/sample/99999-XXXX+%2F%2F+dog+9+fff%C3%A9o+party", Code: 200})
   460  	})
   461  }
   462  
   463  func TestJSVMBase64(t *testing.T) {
   464  	jsvm := JSVM{}
   465  	jsvm.Init(nil, logrus.NewEntry(log))
   466  
   467  	inputString := "teststring"
   468  	inputB64 := "dGVzdHN0cmluZw=="
   469  	jwtPayload := "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
   470  	decodedJwtPayload := `{"sub":"1234567890","name":"John Doe","iat":1516239022}`
   471  
   472  	t.Run("b64dec with simple string input", func(t *testing.T) {
   473  		v, err := jsvm.VM.Run(`b64dec("` + inputB64 + `")`)
   474  		if err != nil {
   475  			t.Fatalf("b64dec call failed: %s", err.Error())
   476  		}
   477  		if s := v.String(); s != inputString {
   478  			t.Fatalf("wanted '%s', got '%s'", inputString, s)
   479  		}
   480  	})
   481  
   482  	t.Run("b64dec with a JWT payload", func(t *testing.T) {
   483  		v, err := jsvm.VM.Run(`b64dec("` + jwtPayload + `")`)
   484  		if err != nil {
   485  			t.Fatalf("b64dec call failed: %s", err.Error())
   486  		}
   487  		if s := v.String(); s != decodedJwtPayload {
   488  			t.Fatalf("wanted '%s', got '%s'", decodedJwtPayload, s)
   489  		}
   490  	})
   491  
   492  	t.Run("b64enc with simple string input", func(t *testing.T) {
   493  		v, err := jsvm.VM.Run(`b64enc("` + inputString + `")`)
   494  		if err != nil {
   495  			t.Fatalf("b64enc call failed: %s", err.Error())
   496  		}
   497  		if s := v.String(); s != inputB64 {
   498  			t.Fatalf("wanted '%s', got '%s'", inputB64, s)
   499  		}
   500  	})
   501  
   502  	t.Run("rawb64dec with simple string input", func(t *testing.T) {
   503  		v, err := jsvm.VM.Run(`rawb64dec("` + jwtPayload + `")`)
   504  		if err != nil {
   505  			t.Fatalf("rawb64dec call failed: %s", err.Error())
   506  		}
   507  		if s := v.String(); s != decodedJwtPayload {
   508  			t.Fatalf("wanted '%s', got '%s'", decodedJwtPayload, s)
   509  		}
   510  	})
   511  
   512  	t.Run("rawb64enc with simple string input", func(t *testing.T) {
   513  		jsvm.VM.Set("input", decodedJwtPayload)
   514  		v, err := jsvm.VM.Run(`rawb64enc(input)`)
   515  		if err != nil {
   516  			t.Fatalf("rawb64enc call failed: %s", err.Error())
   517  		}
   518  		if s := v.String(); s != jwtPayload {
   519  			t.Fatalf("wanted '%s', got '%s'", jwtPayload, s)
   520  		}
   521  	})
   522  }