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 }