mirror of
https://github.com/aykhans/sarin.git
synced 2026-10-16 14:43:53 +00:00
fix(script): reject async/generator JS transforms and catch throwing getters
This commit is contained in:
2 files changed
+73
-23
No files matched your search
+72
-23
@@ -2,6 +2,7 @@ package script
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
@@ -18,6 +19,9 @@ type JsEngine struct {
|
|||||||
http httpBridge
|
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.
|
// 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
|
// The script must define a global `transform` function that takes a request object
|
||||||
// and returns the modified request object.
|
// and returns the modified request object.
|
||||||
@@ -58,6 +62,12 @@ func NewJsEngine(scriptContent string) (*JsEngine, error) {
|
|||||||
return nil, types.NewScriptExecutionError("JavaScript", errors.New("'transform' must be a function"))
|
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
|
engine.transform = transform
|
||||||
|
|
||||||
return engine, nil
|
return engine, nil
|
||||||
@@ -84,6 +94,13 @@ func (e *JsEngine) Transform(req *RequestData) error {
|
|||||||
return types.NewScriptExecutionError("JavaScript", e.exceptionError(err))
|
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
|
// Update RequestData from the returned object
|
||||||
if err := e.objectToRequestData(result, req); err != nil {
|
if err := e.objectToRequestData(result, req); err != nil {
|
||||||
return types.NewScriptExecutionError("JavaScript", err)
|
return types.NewScriptExecutionError("JavaScript", err)
|
||||||
@@ -144,34 +161,40 @@ func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
|
|||||||
return types.ErrScriptTransformReturnObject
|
return types.ErrScriptTransformReturnObject
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method
|
// Reading a field runs the script's own getters and toString, which throw as a Go
|
||||||
if v := obj.Get("method"); v != nil && !goja.IsUndefined(v) {
|
// panic outside goja's execution context, so the whole read runs inside Try.
|
||||||
req.Method = v.String()
|
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
|
// Path
|
||||||
if v := obj.Get("path"); v != nil && !goja.IsUndefined(v) {
|
if v := obj.Get("path"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
|
||||||
req.Path = v.String()
|
req.Path = v.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
if v := obj.Get("body"); v != nil && !goja.IsUndefined(v) {
|
if v := obj.Get("body"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
|
||||||
req.Body = v.String()
|
req.Body = v.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headers
|
// Headers
|
||||||
if v := obj.Get("headers"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
|
if v := obj.Get("headers"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
|
||||||
req.Headers = e.objectToStringSliceMap(v.ToObject(e.runtime))
|
req.Headers = e.objectToStringSliceMap(v.ToObject(e.runtime))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Params
|
// Params
|
||||||
if v := obj.Get("params"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
|
if v := obj.Get("params"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
|
||||||
req.Params = e.objectToStringSliceMap(v.ToObject(e.runtime))
|
req.Params = e.objectToStringSliceMap(v.ToObject(e.runtime))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookies
|
// Cookies
|
||||||
if v := obj.Get("cookies"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
|
if v := obj.Get("cookies"); v != nil && !goja.IsUndefined(v) && !goja.IsNull(v) {
|
||||||
req.Cookies = e.objectToStringSliceMap(v.ToObject(e.runtime))
|
req.Cookies = e.objectToStringSliceMap(v.ToObject(e.runtime))
|
||||||
|
}
|
||||||
|
}); thrown != nil {
|
||||||
|
return e.exceptionError(thrown)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -245,3 +268,29 @@ func jsStringValue(v goja.Value) (string, bool) {
|
|||||||
}
|
}
|
||||||
return v.String(), true
|
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 it is a hint, not a guarantee.
|
||||||
|
// Every read here 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"
|
||||||
|
}
|
||||||
@@ -434,6 +434,7 @@ var (
|
|||||||
ErrScriptSourceEmpty = errors.New("script source cannot be empty after @")
|
ErrScriptSourceEmpty = errors.New("script source cannot be empty after @")
|
||||||
ErrScriptTransformMissing = errors.New("script must define a global 'transform' function")
|
ErrScriptTransformMissing = errors.New("script must define a global 'transform' function")
|
||||||
ErrScriptTransformReturnObject = errors.New("transform function must return an object")
|
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")
|
ErrScriptURLNoHost = errors.New("script URL must have a host")
|
||||||
ErrScriptHTTPOutsideTransform = errors.New("http functions can only be called while transform is running")
|
ErrScriptHTTPOutsideTransform = errors.New("http functions can only be called while transform is running")
|
||||||
ErrScriptHTTPUnavailable = errors.New("http client is not available")
|
ErrScriptHTTPUnavailable = errors.New("http client is not available")
|
||||||
|
|||||||
Reference in new issue
Block a user