mirror of
https://github.com/aykhans/sarin.git
synced 2026-02-28 06:49:13 +00:00
add scripting js/lua
This commit is contained in:
198
internal/script/js.go
Normal file
198
internal/script/js.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
// JsEngine implements the Engine interface using goja (JavaScript).
|
||||
type JsEngine struct {
|
||||
runtime *goja.Runtime
|
||||
transform goja.Callable
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Example JavaScript script:
|
||||
//
|
||||
// function transform(req) {
|
||||
// req.headers["X-Custom"] = "value";
|
||||
// return req;
|
||||
// }
|
||||
func NewJsEngine(scriptContent string) (*JsEngine, error) {
|
||||
vm := goja.New()
|
||||
|
||||
// Execute the script to define the transform function
|
||||
_, err := vm.RunString(scriptContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute JavaScript script: %w", err)
|
||||
}
|
||||
|
||||
// Get the transform function
|
||||
transformVal := vm.Get("transform")
|
||||
if transformVal == nil || goja.IsUndefined(transformVal) || goja.IsNull(transformVal) {
|
||||
return nil, errors.New("script must define a global 'transform' function")
|
||||
}
|
||||
|
||||
transform, ok := goja.AssertFunction(transformVal)
|
||||
if !ok {
|
||||
return nil, errors.New("'transform' must be a function")
|
||||
}
|
||||
|
||||
return &JsEngine{
|
||||
runtime: vm,
|
||||
transform: transform,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Transform executes the JavaScript transform function with the given request data.
|
||||
func (e *JsEngine) Transform(req *RequestData) error {
|
||||
// Convert RequestData to JavaScript object
|
||||
reqObj := e.requestDataToObject(req)
|
||||
|
||||
// Call transform(req)
|
||||
result, err := e.transform(goja.Undefined(), reqObj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("JavaScript transform error: %w", err)
|
||||
}
|
||||
|
||||
// Update RequestData from the returned object
|
||||
if err := e.objectToRequestData(result, req); err != nil {
|
||||
return fmt.Errorf("failed to parse transform result: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close releases the JavaScript runtime resources.
|
||||
func (e *JsEngine) Close() {
|
||||
// goja doesn't have an explicit close method, but we can help GC
|
||||
e.runtime = nil
|
||||
e.transform = nil
|
||||
}
|
||||
|
||||
// requestDataToObject converts RequestData to a goja Value (JavaScript object).
|
||||
func (e *JsEngine) requestDataToObject(req *RequestData) goja.Value {
|
||||
obj := e.runtime.NewObject()
|
||||
|
||||
_ = obj.Set("method", req.Method)
|
||||
_ = obj.Set("url", req.URL)
|
||||
_ = obj.Set("path", req.Path)
|
||||
_ = obj.Set("body", req.Body)
|
||||
|
||||
// Headers (map[string][]string -> object of arrays)
|
||||
headers := e.runtime.NewObject()
|
||||
for k, values := range req.Headers {
|
||||
_ = headers.Set(k, e.stringSliceToArray(values))
|
||||
}
|
||||
_ = obj.Set("headers", headers)
|
||||
|
||||
// Params (map[string][]string -> object of arrays)
|
||||
params := e.runtime.NewObject()
|
||||
for k, values := range req.Params {
|
||||
_ = params.Set(k, e.stringSliceToArray(values))
|
||||
}
|
||||
_ = obj.Set("params", params)
|
||||
|
||||
// Cookies (map[string][]string -> object of arrays)
|
||||
cookies := e.runtime.NewObject()
|
||||
for k, values := range req.Cookies {
|
||||
_ = cookies.Set(k, e.stringSliceToArray(values))
|
||||
}
|
||||
_ = obj.Set("cookies", cookies)
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// objectToRequestData updates RequestData from a JavaScript object.
|
||||
func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
|
||||
if val == nil || goja.IsUndefined(val) || goja.IsNull(val) {
|
||||
return errors.New("transform function must return an object")
|
||||
}
|
||||
|
||||
obj := val.ToObject(e.runtime)
|
||||
if obj == nil {
|
||||
return errors.New("transform function must return an object")
|
||||
}
|
||||
|
||||
// Method
|
||||
if v := obj.Get("method"); v != nil && !goja.IsUndefined(v) {
|
||||
req.Method = v.String()
|
||||
}
|
||||
|
||||
// URL
|
||||
if v := obj.Get("url"); v != nil && !goja.IsUndefined(v) {
|
||||
req.URL = v.String()
|
||||
}
|
||||
|
||||
// Path
|
||||
if v := obj.Get("path"); v != nil && !goja.IsUndefined(v) {
|
||||
req.Path = v.String()
|
||||
}
|
||||
|
||||
// Body
|
||||
if v := obj.Get("body"); v != nil && !goja.IsUndefined(v) {
|
||||
req.Body = v.String()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// stringSliceToArray converts a Go []string to a JavaScript array.
|
||||
func (e *JsEngine) stringSliceToArray(values []string) *goja.Object {
|
||||
ifaces := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
ifaces[i] = v
|
||||
}
|
||||
return e.runtime.NewArray(ifaces...)
|
||||
}
|
||||
|
||||
// objectToStringSliceMap converts a JavaScript object to a Go map[string][]string.
|
||||
// Supports both single string values and array values.
|
||||
func (e *JsEngine) objectToStringSliceMap(obj *goja.Object) map[string][]string {
|
||||
if obj == nil {
|
||||
return make(map[string][]string)
|
||||
}
|
||||
|
||||
result := make(map[string][]string)
|
||||
for _, key := range obj.Keys() {
|
||||
v := obj.Get(key)
|
||||
if v == nil || goja.IsUndefined(v) || goja.IsNull(v) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's an array
|
||||
if arr, ok := v.Export().([]interface{}); ok {
|
||||
var values []string
|
||||
for _, item := range arr {
|
||||
if s, ok := item.(string); ok {
|
||||
values = append(values, s)
|
||||
}
|
||||
}
|
||||
result[key] = values
|
||||
} else {
|
||||
// Single value - wrap in slice
|
||||
result[key] = []string{v.String()}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user