diff --git a/go.mod b/go.mod index 892e357f74..06fe15db6f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/blugelabs/bluge_segment_api v0.2.0 github.com/blugelabs/query_string v0.3.0 github.com/dop251/goja v0.0.0-20220806120448-1444e6b94559 + github.com/dop251/goja_nodejs v0.0.0-20220808115320-bac29516aae9 github.com/gofrs/uuid v4.0.0+incompatible github.com/golang-jwt/jwt/v4 v4.1.0 github.com/gorilla/handlers v1.5.1 diff --git a/go.sum b/go.sum index ccd7a9753b..4fcacdd046 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,8 @@ github.com/dop251/goja v0.0.0-20220806120448-1444e6b94559 h1:S3U65m9SN2p5CJpT3CD github.com/dop251/goja v0.0.0-20220806120448-1444e6b94559/go.mod h1:1jWwHOtOkEqsfX6tYsufUc7BBTuGHH2ekiJabpkN4CA= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= +github.com/dop251/goja_nodejs v0.0.0-20220808115320-bac29516aae9 h1:7nszERfxMR5Gyw+M21EbrZZyTtVbRhNdRmtW/Vr3hzc= +github.com/dop251/goja_nodejs v0.0.0-20220808115320-bac29516aae9/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= diff --git a/server/runtime_javascript.go b/server/runtime_javascript.go index 4713fda608..62051fa442 100644 --- a/server/runtime_javascript.go +++ b/server/runtime_javascript.go @@ -28,6 +28,8 @@ import ( "github.com/dop251/goja" "github.com/dop251/goja/ast" + "github.com/dop251/goja_nodejs/process" + "github.com/dop251/goja_nodejs/require" "github.com/gofrs/uuid" "github.com/heroiclabs/nakama-common/api" "github.com/heroiclabs/nakama-common/rtapi" @@ -1986,6 +1988,12 @@ func evalRuntimeModules(rp *RuntimeProviderJS, modCache *RuntimeJSModuleCache, m logger := rp.logger r := goja.New() + // Quite a few JavaScript that were originally written with node.js + // in mind test for something like `process.env.NODE_ENV === "production"`. + // The goja_nodejs libraries provide a subset of the node.js API to + // mitigate these issues. + new(require.Registry).Enable(r) + process.Enable(r) callbacks := &RuntimeJavascriptCallbacks{ Rpc: make(map[string]string), diff --git a/server/runtime_javascript_test.go b/server/runtime_javascript_test.go index e50eab6eb1..d52591ea81 100644 --- a/server/runtime_javascript_test.go +++ b/server/runtime_javascript_test.go @@ -112,3 +112,13 @@ m.get('a'); } }) } + +func TestMinimalProcessEnv(t *testing.T) { + vm := goja.New() + + _, err := vm.RunString("if (process.env.NODE_ENV === 'production') { }") + + if err != nil { + t.Fatal("Legit code involving process.env failed") + } +} diff --git a/vendor/github.com/dop251/goja_nodejs/LICENSE b/vendor/github.com/dop251/goja_nodejs/LICENSE new file mode 100644 index 0000000000..8c27a94109 --- /dev/null +++ b/vendor/github.com/dop251/goja_nodejs/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2016 Dmitry Panov + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/dop251/goja_nodejs/process/module.go b/vendor/github.com/dop251/goja_nodejs/process/module.go new file mode 100644 index 0000000000..99df44e06e --- /dev/null +++ b/vendor/github.com/dop251/goja_nodejs/process/module.go @@ -0,0 +1,35 @@ +package process + +import ( + "os" + "strings" + + "github.com/dop251/goja" + "github.com/dop251/goja_nodejs/require" +) + +type Process struct { + env map[string]string +} + +func Require(runtime *goja.Runtime, module *goja.Object) { + p := &Process{ + env: make(map[string]string), + } + + for _, e := range os.Environ() { + envKeyValue := strings.SplitN(e, "=", 2) + p.env[envKeyValue[0]] = envKeyValue[1] + } + + o := module.Get("exports").(*goja.Object) + o.Set("env", p.env) +} + +func Enable(runtime *goja.Runtime) { + runtime.Set("process", require.Require(runtime, "process")) +} + +func init() { + require.RegisterNativeModule("process", Require) +} diff --git a/vendor/github.com/dop251/goja_nodejs/require/module.go b/vendor/github.com/dop251/goja_nodejs/require/module.go new file mode 100644 index 0000000000..656a3f992e --- /dev/null +++ b/vendor/github.com/dop251/goja_nodejs/require/module.go @@ -0,0 +1,214 @@ +package require + +import ( + "errors" + "io/ioutil" + "os" + "path" + "path/filepath" + "runtime" + "sync" + "syscall" + "text/template" + + js "github.com/dop251/goja" + "github.com/dop251/goja/parser" +) + +type ModuleLoader func(*js.Runtime, *js.Object) + +// SourceLoader represents a function that returns a file data at a given path. +// The function should return ModuleFileDoesNotExistError if the file either doesn't exist or is a directory. +// This error will be ignored by the resolver and the search will continue. Any other errors will be propagated. +type SourceLoader func(path string) ([]byte, error) + +var ( + InvalidModuleError = errors.New("Invalid module") + IllegalModuleNameError = errors.New("Illegal module name") + + ModuleFileDoesNotExistError = errors.New("module file does not exist") +) + +var native map[string]ModuleLoader + +// Registry contains a cache of compiled modules which can be used by multiple Runtimes +type Registry struct { + sync.Mutex + native map[string]ModuleLoader + compiled map[string]*js.Program + + srcLoader SourceLoader + globalFolders []string +} + +type RequireModule struct { + r *Registry + runtime *js.Runtime + modules map[string]*js.Object + nodeModules map[string]*js.Object +} + +func NewRegistry(opts ...Option) *Registry { + r := &Registry{} + + for _, opt := range opts { + opt(r) + } + + return r +} + +func NewRegistryWithLoader(srcLoader SourceLoader) *Registry { + return NewRegistry(WithLoader(srcLoader)) +} + +type Option func(*Registry) + +// WithLoader sets a function which will be called by the require() function in order to get a source code for a +// module at the given path. The same function will be used to get external source maps. +// Note, this only affects the modules loaded by the require() function. If you need to use it as a source map +// loader for code parsed in a different way (such as runtime.RunString() or eval()), use (*Runtime).SetParserOptions() +func WithLoader(srcLoader SourceLoader) Option { + return func(r *Registry) { + r.srcLoader = srcLoader + } +} + +// WithGlobalFolders appends the given paths to the registry's list of +// global folders to search if the requested module is not found +// elsewhere. By default, a registry's global folders list is empty. +// In the reference Node.js implementation, the default global folders +// list is $NODE_PATH, $HOME/.node_modules, $HOME/.node_libraries and +// $PREFIX/lib/node, see +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders. +func WithGlobalFolders(globalFolders ...string) Option { + return func(r *Registry) { + r.globalFolders = globalFolders + } +} + +// Enable adds the require() function to the specified runtime. +func (r *Registry) Enable(runtime *js.Runtime) *RequireModule { + rrt := &RequireModule{ + r: r, + runtime: runtime, + modules: make(map[string]*js.Object), + nodeModules: make(map[string]*js.Object), + } + + runtime.Set("require", rrt.require) + return rrt +} + +func (r *Registry) RegisterNativeModule(name string, loader ModuleLoader) { + r.Lock() + defer r.Unlock() + + if r.native == nil { + r.native = make(map[string]ModuleLoader) + } + name = filepathClean(name) + r.native[name] = loader +} + +// DefaultSourceLoader is used if none was set (see WithLoader()). It simply loads files from the host's filesystem. +func DefaultSourceLoader(filename string) ([]byte, error) { + fp := filepath.FromSlash(filename) + data, err := ioutil.ReadFile(fp) + if err != nil { + if os.IsNotExist(err) || errors.Is(err, syscall.EISDIR) { + err = ModuleFileDoesNotExistError + } else if runtime.GOOS == "windows" { // temporary workaround for https://github.com/dop251/goja_nodejs/issues/21 + fi, err1 := os.Stat(fp) + if err1 == nil && fi.IsDir() { + err = ModuleFileDoesNotExistError + } + } + } + return data, err +} + +func (r *Registry) getSource(p string) ([]byte, error) { + srcLoader := r.srcLoader + if srcLoader == nil { + srcLoader = DefaultSourceLoader + } + return srcLoader(p) +} + +func (r *Registry) getCompiledSource(p string) (*js.Program, error) { + r.Lock() + defer r.Unlock() + + prg := r.compiled[p] + if prg == nil { + buf, err := r.getSource(p) + if err != nil { + return nil, err + } + s := string(buf) + + if path.Ext(p) == ".json" { + s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')" + } + + source := "(function(exports, require, module) {" + s + "\n})" + parsed, err := js.Parse(p, source, parser.WithSourceMapLoader(r.srcLoader)) + if err != nil { + return nil, err + } + prg, err = js.CompileAST(parsed, false) + if err == nil { + if r.compiled == nil { + r.compiled = make(map[string]*js.Program) + } + r.compiled[p] = prg + } + return prg, err + } + return prg, nil +} + +func (r *RequireModule) require(call js.FunctionCall) js.Value { + ret, err := r.Require(call.Argument(0).String()) + if err != nil { + if _, ok := err.(*js.Exception); !ok { + panic(r.runtime.NewGoError(err)) + } + panic(err) + } + return ret +} + +func filepathClean(p string) string { + return path.Clean(p) +} + +// Require can be used to import modules from Go source (similar to JS require() function). +func (r *RequireModule) Require(p string) (ret js.Value, err error) { + module, err := r.resolve(p) + if err != nil { + return + } + ret = module.Get("exports") + return +} + +func Require(runtime *js.Runtime, name string) js.Value { + if r, ok := js.AssertFunction(runtime.Get("require")); ok { + mod, err := r(js.Undefined(), runtime.ToValue(name)) + if err != nil { + panic(err) + } + return mod + } + panic(runtime.NewTypeError("Please enable require for this runtime using new(require.Registry).Enable(runtime)")) +} + +func RegisterNativeModule(name string, loader ModuleLoader) { + if native == nil { + native = make(map[string]ModuleLoader) + } + name = filepathClean(name) + native[name] = loader +} diff --git a/vendor/github.com/dop251/goja_nodejs/require/resolve.go b/vendor/github.com/dop251/goja_nodejs/require/resolve.go new file mode 100644 index 0000000000..6c24175270 --- /dev/null +++ b/vendor/github.com/dop251/goja_nodejs/require/resolve.go @@ -0,0 +1,231 @@ +package require + +import ( + "encoding/json" + "errors" + "path" + "strings" + + js "github.com/dop251/goja" +) + +// NodeJS module search algorithm described by +// https://nodejs.org/api/modules.html#modules_all_together +func (r *RequireModule) resolve(modpath string) (module *js.Object, err error) { + origPath, modpath := modpath, filepathClean(modpath) + if modpath == "" { + return nil, IllegalModuleNameError + } + + module, err = r.loadNative(modpath) + if err == nil { + return + } + + var start string + err = nil + if path.IsAbs(origPath) { + start = "/" + } else { + start = r.getCurrentModulePath() + } + + p := path.Join(start, modpath) + if strings.HasPrefix(origPath, "./") || + strings.HasPrefix(origPath, "/") || strings.HasPrefix(origPath, "../") || + origPath == "." || origPath == ".." { + if module = r.modules[p]; module != nil { + return + } + module, err = r.loadAsFileOrDirectory(p) + if err == nil && module != nil { + r.modules[p] = module + } + } else { + if module = r.nodeModules[p]; module != nil { + return + } + module, err = r.loadNodeModules(modpath, start) + if err == nil && module != nil { + r.nodeModules[p] = module + } + } + + if module == nil && err == nil { + err = InvalidModuleError + } + return +} + +func (r *RequireModule) loadNative(path string) (*js.Object, error) { + module := r.modules[path] + if module != nil { + return module, nil + } + + var ldr ModuleLoader + if ldr = r.r.native[path]; ldr == nil { + ldr = native[path] + } + + if ldr != nil { + module = r.createModuleObject() + r.modules[path] = module + ldr(r.runtime, module) + return module, nil + } + + return nil, InvalidModuleError +} + +func (r *RequireModule) loadAsFileOrDirectory(path string) (module *js.Object, err error) { + if module, err = r.loadAsFile(path); module != nil || err != nil { + return + } + + return r.loadAsDirectory(path) +} + +func (r *RequireModule) loadAsFile(path string) (module *js.Object, err error) { + if module, err = r.loadModule(path); module != nil || err != nil { + return + } + + p := path + ".js" + if module, err = r.loadModule(p); module != nil || err != nil { + return + } + + p = path + ".json" + return r.loadModule(p) +} + +func (r *RequireModule) loadIndex(modpath string) (module *js.Object, err error) { + p := path.Join(modpath, "index.js") + if module, err = r.loadModule(p); module != nil || err != nil { + return + } + + p = path.Join(modpath, "index.json") + return r.loadModule(p) +} + +func (r *RequireModule) loadAsDirectory(modpath string) (module *js.Object, err error) { + p := path.Join(modpath, "package.json") + buf, err := r.r.getSource(p) + if err != nil { + return r.loadIndex(modpath) + } + var pkg struct { + Main string + } + err = json.Unmarshal(buf, &pkg) + if err != nil || len(pkg.Main) == 0 { + return r.loadIndex(modpath) + } + + m := path.Join(modpath, pkg.Main) + if module, err = r.loadAsFile(m); module != nil || err != nil { + return + } + + return r.loadIndex(m) +} + +func (r *RequireModule) loadNodeModule(modpath, start string) (*js.Object, error) { + return r.loadAsFileOrDirectory(path.Join(start, modpath)) +} + +func (r *RequireModule) loadNodeModules(modpath, start string) (module *js.Object, err error) { + for _, dir := range r.r.globalFolders { + if module, err = r.loadNodeModule(modpath, dir); module != nil || err != nil { + return + } + } + for { + var p string + if path.Base(start) != "node_modules" { + p = path.Join(start, "node_modules") + } else { + p = start + } + if module, err = r.loadNodeModule(modpath, p); module != nil || err != nil { + return + } + if start == ".." { // Dir('..') is '.' + break + } + parent := path.Dir(start) + if parent == start { + break + } + start = parent + } + + return nil, InvalidModuleError +} + +func (r *RequireModule) getCurrentModulePath() string { + var buf [2]js.StackFrame + frames := r.runtime.CaptureCallStack(2, buf[:0]) + if len(frames) < 2 { + return "." + } + return path.Dir(frames[1].SrcName()) +} + +func (r *RequireModule) createModuleObject() *js.Object { + module := r.runtime.NewObject() + module.Set("exports", r.runtime.NewObject()) + return module +} + +func (r *RequireModule) loadModule(path string) (*js.Object, error) { + module := r.modules[path] + if module == nil { + module = r.createModuleObject() + r.modules[path] = module + err := r.loadModuleFile(path, module) + if err != nil { + module = nil + delete(r.modules, path) + if errors.Is(err, ModuleFileDoesNotExistError) { + err = nil + } + } + return module, err + } + return module, nil +} + +func (r *RequireModule) loadModuleFile(path string, jsModule *js.Object) error { + + prg, err := r.r.getCompiledSource(path) + + if err != nil { + return err + } + + f, err := r.runtime.RunProgram(prg) + if err != nil { + return err + } + + if call, ok := js.AssertFunction(f); ok { + jsExports := jsModule.Get("exports") + jsRequire := r.runtime.Get("require") + + // Run the module source, with "jsExports" as "this", + // "jsExports" as the "exports" variable, "jsRequire" + // as the "require" variable and "jsModule" as the + // "module" variable (Nodejs capable). + _, err = call(jsExports, jsExports, jsRequire, jsModule) + if err != nil { + return err + } + } else { + return InvalidModuleError + } + + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ec5d59217d..a932e3ed23 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -83,6 +83,10 @@ github.com/dop251/goja/ftoa/internal/fast github.com/dop251/goja/parser github.com/dop251/goja/token github.com/dop251/goja/unistring +# github.com/dop251/goja_nodejs v0.0.0-20220808115320-bac29516aae9 +## explicit; go 1.14 +github.com/dop251/goja_nodejs/process +github.com/dop251/goja_nodejs/require # github.com/felixge/httpsnoop v1.0.1 ## explicit; go 1.13 github.com/felixge/httpsnoop