mirror of
https://github.com/aykhans/sarin.git
synced 2026-10-13 21:28:52 +00:00
Compare commits
2
Commits
e85a6d4613
...
9e773a4e6c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e773a4e6c
|
||
|
|
bcf2ea3446
|
No files matched your search
+105
-29
@@ -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"
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user