Compare commits

...
2 Commits
2 changed files with 106 additions and 29 deletions

No files matched your search

+105 -29
View File
@@ -2,6 +2,8 @@ package script
import (
"errors"
"fmt"
"reflect"
"strconv"
"github.com/dop251/goja"
@@ -18,6 +20,9 @@ type JsEngine struct {
http httpBridge
}
// promiseType is what a transform returns when it is declared async or as a generator.
var promiseType = reflect.TypeFor[*goja.Promise]()
// NewJsEngine creates a new JavaScript script engine with the given script content.
// The script must define a global `transform` function that takes a request object
// and returns the modified request object.
@@ -42,8 +47,7 @@ func NewJsEngine(scriptContent string) (*JsEngine, error) {
}
// Execute the script to define the transform function
_, err := vm.RunString(scriptContent)
if err != nil {
if _, err := vm.RunString(scriptContent); err != nil {
return nil, types.NewScriptExecutionError("JavaScript", engine.exceptionError(err))
}
@@ -58,6 +62,12 @@ func NewJsEngine(scriptContent string) (*JsEngine, error) {
return nil, types.NewScriptExecutionError("JavaScript", errors.New("'transform' must be a function"))
}
// A generator returns an iterator, which is an ordinary object to Go, so the
// call cannot tell it apart and the declaration is the only thing left to read.
if isGeneratorFunction(vm, transformVal) {
return nil, types.ErrScriptTransformAsync
}
engine.transform = transform
return engine, nil
@@ -84,6 +94,13 @@ func (e *JsEngine) Transform(req *RequestData) error {
return types.NewScriptExecutionError("JavaScript", e.exceptionError(err))
}
// An async or generator transform returns a promise nothing ever awaits, and a promise
// carries none of the request fields, so the request would go out untransformed.
// ExportType reads the type alone, while Export would run every getter on the result.
if result.ExportType() == promiseType {
return types.ErrScriptTransformAsync
}
// Update RequestData from the returned object
if err := e.objectToRequestData(result, req); err != nil {
return types.NewScriptExecutionError("JavaScript", err)
@@ -144,37 +161,70 @@ func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
return types.ErrScriptTransformReturnObject
}
// Method
if v := obj.Get("method"); v != nil && !goja.IsUndefined(v) {
req.Method = v.String()
// Reading a field runs the script's own getters and toString, which throw as a Go
// panic outside goja's execution context, so the whole read runs inside Try.
var fieldErr error
if thrown := e.runtime.Try(func() {
// Method
if v := obj.Get("method"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Method = v.String()
}
// Path
if v := obj.Get("path"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Path = v.String()
}
// Body
if v := obj.Get("body"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Body = v.String()
}
// Headers
if v := obj.Get("headers"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
values, err := objectField("headers", v)
if err != nil {
fieldErr = err
return
}
req.Headers = e.objectToStringSliceMap(values)
}
// Params
if v := obj.Get("params"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
values, err := objectField("params", v)
if err != nil {
fieldErr = err
return
}
req.Params = e.objectToStringSliceMap(values)
}
// Cookies
if v := obj.Get("cookies"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
values, err := objectField("cookies", v)
if err != nil {
fieldErr = err
return
}
req.Cookies = e.objectToStringSliceMap(values)
}
}); thrown != nil {
return e.exceptionError(thrown)
}
// Path
if v := obj.Get("path"); v != nil && !goja.IsUndefined(v) {
req.Path = v.String()
}
return fieldErr
}
// Body
if v := obj.Get("body"); v != nil && !goja.IsUndefined(v) {
req.Body = v.String()
// objectField rejects anything that is not a plain object, since reading keys off an
// array or a boxed string yields headers named after indexes and off a Date yields none.
// A Map, a Set and a typed array still pass, so the field is emptied rather than refused.
func objectField(name string, value goja.Value) (*goja.Object, error) {
obj, ok := value.(*goja.Object)
if !ok || obj.ClassName() != "Object" {
return nil, fmt.Errorf("%s must be an object", name)
}
// Headers
if v := obj.Get("headers"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Headers = e.objectToStringSliceMap(v.ToObject(e.runtime))
}
// Params
if v := obj.Get("params"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Params = e.objectToStringSliceMap(v.ToObject(e.runtime))
}
// Cookies
if v := obj.Get("cookies"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
req.Cookies = e.objectToStringSliceMap(v.ToObject(e.runtime))
}
return nil
return obj, nil
}
// stringSliceToArray converts a Go []string to a JavaScript array.
@@ -245,3 +295,29 @@ func jsStringValue(v goja.Value) (string, bool) {
}
return v.String(), true
}
// isGeneratorFunction reports whether a function is declared with a star.
// The name comes from the script's own prototype chain, so a script can falsify it either way.
// Reading it can run a script getter, so the whole walk stays inside Try.
func isGeneratorFunction(runtime *goja.Runtime, value goja.Value) bool {
var name string
thrown := runtime.Try(func() {
obj := value.ToObject(runtime)
if obj == nil {
return
}
constructor := obj.Get("constructor")
if constructor == nil || goja.IsUndefined(constructor) || goja.IsNull(constructor) {
return
}
constructorObj := constructor.ToObject(runtime)
if constructorObj == nil {
return
}
if value := constructorObj.Get("name"); value != nil {
name = value.String()
}
})
return thrown == nil && name == "GeneratorFunction"
}
+1
View File
@@ -434,6 +434,7 @@ var (
ErrScriptSourceEmpty = errors.New("script source cannot be empty after @")
ErrScriptTransformMissing = errors.New("script must define a global 'transform' function")
ErrScriptTransformReturnObject = errors.New("transform function must return an object")
ErrScriptTransformAsync = errors.New("transform function must be synchronous, async and generator functions are not supported")
ErrScriptURLNoHost = errors.New("script URL must have a host")
ErrScriptHTTPOutsideTransform = errors.New("http functions can only be called while transform is running")
ErrScriptHTTPUnavailable = errors.New("http client is not available")