diff --git a/README.md b/README.md index 4e9e5d7..31d4b8c 100644 --- a/README.md +++ b/README.md @@ -385,6 +385,10 @@ Following builtin functions are addionally available within JS scripts: * `print(val)` - print arbitrary values *val* into dumbproxy log for debugging purposes. * `readFile(path: string): string` - read file from *path* and return its content as a string. +Following objects are additionally available in global scope of JS scripts: + +* `env` - readonly object containing all environment variables. + ## Supported upstream proxy schemes Supported proxy schemes are: diff --git a/access/jsfilter.go b/access/jsfilter.go index 208ed5c..eec1af3 100644 --- a/access/jsfilter.go +++ b/access/jsfilter.go @@ -37,11 +37,11 @@ func NewJSFilter(filename string, instances int, logger *clog.CondLogger, next F vm := goja.New() err = jsext.AddPrinter(vm, logger) if err != nil { - return nil, errors.New("can't add print function to runtime") + return nil, fmt.Errorf("can't add print function to runtime: %w", err) } - err = jsext.AddFileReader(vm) + err = jsext.ConfigureRuntime(vm) if err != nil { - return nil, errors.New("can't add file reader function to runtime") + return nil, fmt.Errorf("can't configure runtime: %w", err) } vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) _, err = vm.RunString(string(script)) diff --git a/dialer/jsrouter.go b/dialer/jsrouter.go index de32acd..8c41b12 100644 --- a/dialer/jsrouter.go +++ b/dialer/jsrouter.go @@ -35,11 +35,11 @@ func NewJSRouter(filename string, instances int, factory func(string) (Dialer, e vm := goja.New() err := jsext.AddPrinter(vm, logger) if err != nil { - return nil, errors.New("can't add print function to runtime") + return nil, fmt.Errorf("can't add print function to runtime: %w", err) } - err = jsext.AddFileReader(vm) + err = jsext.ConfigureRuntime(vm) if err != nil { - return nil, errors.New("can't add file reader function to runtime") + return nil, fmt.Errorf("can't configure runtime runtime: %w", err) } vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) _, err = vm.RunString(string(script)) diff --git a/jsext/env.go b/jsext/env.go new file mode 100644 index 0000000..7c76a70 --- /dev/null +++ b/jsext/env.go @@ -0,0 +1,74 @@ +package jsext + +import ( + "os" + "slices" + "strings" + "sync" + + "github.com/dop251/goja" +) + +var ( + createEnvObjectOnce sync.Once + envObject *goja.Object +) + +type readonlyEnvObject struct { + m map[string]goja.String + k []string +} + +func (o *readonlyEnvObject) Get(key string) goja.Value { + v, ok := o.m[key] + if ok { + return v + } + return goja.Undefined() +} + +func (o *readonlyEnvObject) Set(_ string, _ goja.Value) bool { + return false +} + +func (o *readonlyEnvObject) Has(key string) bool { + _, ok := o.m[key] + return ok +} + +func (o *readonlyEnvObject) Delete(key string) bool { + return false +} + +func (o *readonlyEnvObject) Keys() []string { + return o.k +} + +func createEnvObject() *goja.Object { + env := os.Environ() + m := make(map[string]goja.String, len(env)) + k := make([]string, 0, len(env)) + for _, pair := range env { + key, value, _ := strings.Cut(pair, "=") + sb := new(goja.StringBuilder) + sb.WriteUTF8String(value) + m[key] = sb.String() + k = append(k, key) + } + slices.Sort(k) + return goja.NewSharedDynamicObject(&readonlyEnvObject{ + m: m, + k: k, + }) +} + +func GetEnvSharedDynamicObject() *goja.Object { + createEnvObjectOnce.Do(func() { + envObject = createEnvObject() + }) + return envObject +} + +func ExportEnv(vm *goja.Runtime) error { + return vm.Set("env", GetEnvSharedDynamicObject()) +} diff --git a/jsext/jsext.go b/jsext/jsext.go new file mode 100644 index 0000000..d4479c6 --- /dev/null +++ b/jsext/jsext.go @@ -0,0 +1,13 @@ +package jsext + +import "github.com/dop251/goja" + +func ConfigureRuntime(vm *goja.Runtime) error { + if err := AddFileReader(vm); err != nil { + return err + } + if err := ExportEnv(vm); err != nil { + return err + } + return nil +}