nuclei/pkg/js/compiler/pool.go

258 lines
8.7 KiB
Go
Raw Permalink Normal View History

package compiler
import (
"bytes"
"context"
"fmt"
"reflect"
"sync"
Remove singletons from Nuclei engine (continuation of #6210) (#6296) * introducing execution id * wip * . * adding separate execution context id * lint * vet * fixing pg dialers * test ignore * fixing loader FD limit * test * fd fix * wip: remove CloseProcesses() from dev merge * wip: fix merge issue * protocolstate: stop memguarding on last dialer delete * avoid data race in dialers.RawHTTPClient * use shared logger and avoid race conditions * use shared logger and avoid race conditions * go mod * patch executionId into compiled template cache * clean up comment in Parse * go mod update * bump echarts * address merge issues * fix use of gologger * switch cmd/nuclei to options.Logger * address merge issues with go.mod * go vet: address copy of lock with new Copy function * fixing tests * disable speed control * fix nil ExecuterOptions * removing deprecated code * fixing result print * default logger * cli default logger * filter warning from results * fix performance test * hardcoding path * disable upload * refactor(runner): uses `Warning` instead of `Print` for `pdcpUploadErrMsg` Signed-off-by: Dwi Siswanto <git@dw1.io> * Revert "disable upload" This reverts commit 114fbe6663361bf41cf8b2645fd2d57083d53682. * Revert "hardcoding path" This reverts commit cf12ca800e0a0e974bd9fd4826a24e51547f7c00. --------- Signed-off-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Mzack9999 <mzack9999@protonmail.com> Co-authored-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Dwi Siswanto <25837540+dwisiswant0@users.noreply.github.com>
2025-07-09 14:47:26 -05:00
"github.com/Mzack9999/goja"
"github.com/Mzack9999/goja_nodejs/console"
"github.com/Mzack9999/goja_nodejs/require"
"github.com/kitabisa/go-ci"
"github.com/projectdiscovery/gologger"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libbytes"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libfs"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libikev2"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libkerberos"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libldap"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libmssql"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libmysql"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libnet"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/liboracle"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libpop3"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libpostgres"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/librdp"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libredis"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/librsync"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libsmb"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libsmtp"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libssh"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libstructs"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libtelnet"
_ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libvnc"
"github.com/projectdiscovery/nuclei/v3/pkg/js/global"
"github.com/projectdiscovery/nuclei/v3/pkg/js/gojs"
"github.com/projectdiscovery/nuclei/v3/pkg/js/libs/goconsole"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
stringsutil "github.com/projectdiscovery/utils/strings"
2024-04-03 17:50:57 +02:00
syncutil "github.com/projectdiscovery/utils/sync"
)
const (
exportToken = "Export"
exportAsToken = "ExportAs"
)
var (
r *require.Registry
lazyRegistryInit = sync.OnceFunc(func() {
r = new(require.Registry) // this can be shared by multiple runtimes
// autoregister console node module with default printer it uses gologger backend
require.RegisterNativeModule(console.ModuleName, console.RequireWithPrinter(goconsole.NewGoConsolePrinter()))
})
2024-04-03 17:50:57 +02:00
pooljsc *syncutil.AdaptiveWaitGroup
lazySgInit = sync.OnceFunc(func() {
2024-04-03 17:50:57 +02:00
pooljsc, _ = syncutil.New(syncutil.WithSize(PoolingJsVmConcurrency))
})
sgResizeCheck = func(ctx context.Context) {
2024-04-03 19:02:30 +02:00
// resize check point
if pooljsc.Size != PoolingJsVmConcurrency {
if err := pooljsc.Resize(ctx, PoolingJsVmConcurrency); err != nil {
gologger.Warning().Msgf("Could not resize workpool: %s\n", err)
}
2024-04-03 19:02:30 +02:00
}
}
)
var gojapool = &sync.Pool{
New: func() interface{} {
return createNewRuntime()
},
}
func executeWithRuntime(runtime *goja.Runtime, p *goja.Program, args *ExecuteArgs, opts *ExecuteOptions) (result goja.Value, err error) {
defer func() {
// reset before putting back to pool
_ = runtime.GlobalObject().Delete("template") // template ctx
// remove all args
for k := range args.Args {
_ = runtime.GlobalObject().Delete(k)
}
if opts != nil && opts.Cleanup != nil {
opts.Cleanup(runtime)
}
Remove singletons from Nuclei engine (continuation of #6210) (#6296) * introducing execution id * wip * . * adding separate execution context id * lint * vet * fixing pg dialers * test ignore * fixing loader FD limit * test * fd fix * wip: remove CloseProcesses() from dev merge * wip: fix merge issue * protocolstate: stop memguarding on last dialer delete * avoid data race in dialers.RawHTTPClient * use shared logger and avoid race conditions * use shared logger and avoid race conditions * go mod * patch executionId into compiled template cache * clean up comment in Parse * go mod update * bump echarts * address merge issues * fix use of gologger * switch cmd/nuclei to options.Logger * address merge issues with go.mod * go vet: address copy of lock with new Copy function * fixing tests * disable speed control * fix nil ExecuterOptions * removing deprecated code * fixing result print * default logger * cli default logger * filter warning from results * fix performance test * hardcoding path * disable upload * refactor(runner): uses `Warning` instead of `Print` for `pdcpUploadErrMsg` Signed-off-by: Dwi Siswanto <git@dw1.io> * Revert "disable upload" This reverts commit 114fbe6663361bf41cf8b2645fd2d57083d53682. * Revert "hardcoding path" This reverts commit cf12ca800e0a0e974bd9fd4826a24e51547f7c00. --------- Signed-off-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Mzack9999 <mzack9999@protonmail.com> Co-authored-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Dwi Siswanto <25837540+dwisiswant0@users.noreply.github.com>
2025-07-09 14:47:26 -05:00
runtime.RemoveContextValue("executionId")
}()
// TODO(dwisiswant0): remove this once we get the RCA.
defer func() {
if ci.IsCI() {
return
}
if r := recover(); r != nil {
err = fmt.Errorf("panic: %s", r)
}
}()
// set template ctx
_ = runtime.Set("template", args.TemplateCtx)
// set args
for k, v := range args.Args {
_ = runtime.Set(k, v)
}
// register extra callbacks if any
if opts != nil && opts.Callback != nil {
if err := opts.Callback(runtime); err != nil {
return nil, err
}
}
Remove singletons from Nuclei engine (continuation of #6210) (#6296) * introducing execution id * wip * . * adding separate execution context id * lint * vet * fixing pg dialers * test ignore * fixing loader FD limit * test * fd fix * wip: remove CloseProcesses() from dev merge * wip: fix merge issue * protocolstate: stop memguarding on last dialer delete * avoid data race in dialers.RawHTTPClient * use shared logger and avoid race conditions * use shared logger and avoid race conditions * go mod * patch executionId into compiled template cache * clean up comment in Parse * go mod update * bump echarts * address merge issues * fix use of gologger * switch cmd/nuclei to options.Logger * address merge issues with go.mod * go vet: address copy of lock with new Copy function * fixing tests * disable speed control * fix nil ExecuterOptions * removing deprecated code * fixing result print * default logger * cli default logger * filter warning from results * fix performance test * hardcoding path * disable upload * refactor(runner): uses `Warning` instead of `Print` for `pdcpUploadErrMsg` Signed-off-by: Dwi Siswanto <git@dw1.io> * Revert "disable upload" This reverts commit 114fbe6663361bf41cf8b2645fd2d57083d53682. * Revert "hardcoding path" This reverts commit cf12ca800e0a0e974bd9fd4826a24e51547f7c00. --------- Signed-off-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Mzack9999 <mzack9999@protonmail.com> Co-authored-by: Dwi Siswanto <git@dw1.io> Co-authored-by: Dwi Siswanto <25837540+dwisiswant0@users.noreply.github.com>
2025-07-09 14:47:26 -05:00
// inject execution id and context
runtime.SetContextValue("executionId", opts.ExecutionId)
// execute the script
return runtime.RunProgram(p)
}
// ExecuteProgram executes a compiled program with the default options.
// it deligates if a particular program should run in a pooled or non-pooled runtime
func ExecuteProgram(p *goja.Program, args *ExecuteArgs, opts *ExecuteOptions) (result goja.Value, err error) {
if opts.Source == nil {
// not-recommended anymore
return executeWithoutPooling(p, args, opts)
}
if !stringsutil.ContainsAny(*opts.Source, exportAsToken, exportToken) {
// not-recommended anymore
return executeWithoutPooling(p, args, opts)
}
return executeWithPoolingProgram(p, args, opts)
}
// executes the actual js program
func executeWithPoolingProgram(p *goja.Program, args *ExecuteArgs, opts *ExecuteOptions) (result goja.Value, err error) {
// its unknown (most likely cannot be done) to limit max js runtimes at a moment without making it static
// unlike sync.Pool which reacts to GC and its purposes is to reuse objects rather than creating new ones
lazySgInit()
sgResizeCheck(opts.Context)
2024-04-03 19:02:30 +02:00
pooljsc.Add()
defer pooljsc.Done()
runtime := gojapool.Get().(*goja.Runtime)
defer gojapool.Put(runtime)
var buff bytes.Buffer
opts.exports = make(map[string]interface{})
defer func() {
// remove below functions from runtime
_ = runtime.GlobalObject().Delete(exportAsToken)
_ = runtime.GlobalObject().Delete(exportToken)
}()
// register export functions
_ = gojs.RegisterFuncWithSignature(runtime, gojs.FuncOpts{
Name: "Export", // we use string instead of const for documentation generation
Signatures: []string{"Export(value any)"},
Description: "Converts a given value to a string and is appended to output of script",
FuncDecl: func(call goja.FunctionCall, runtime *goja.Runtime) goja.Value {
if len(call.Arguments) == 0 {
return goja.Null()
}
for _, arg := range call.Arguments {
if out := stringify(arg, runtime); out != "" {
buff.WriteString(out)
}
}
return goja.Null()
},
})
// register exportAs function
_ = gojs.RegisterFuncWithSignature(runtime, gojs.FuncOpts{
Name: "ExportAs", // Export
Signatures: []string{"ExportAs(key string,value any)"},
Description: "Exports given value with specified key and makes it available in DSL and response",
FuncDecl: func(call goja.FunctionCall, runtime *goja.Runtime) goja.Value {
if len(call.Arguments) != 2 {
// this is how goja expects errors to be returned
// and internally it is done same way for all errors
panic(runtime.ToValue("ExportAs expects 2 arguments"))
}
key := call.Argument(0).String()
value := call.Argument(1)
opts.exports[key] = stringify(value, runtime)
return goja.Null()
},
})
val, err := executeWithRuntime(runtime, p, args, opts)
if err != nil {
return nil, err
}
if val.Export() != nil {
// append last value to output
buff.WriteString(stringify(val, runtime))
}
// and return it as result
return runtime.ToValue(buff.String()), nil
}
// Internal purposes i.e generating bindings
func InternalGetGeneratorRuntime() *goja.Runtime {
runtime := gojapool.Get().(*goja.Runtime)
return runtime
}
func getRegistry() *require.Registry {
lazyRegistryInit()
return r
}
func createNewRuntime() *goja.Runtime {
runtime := protocolstate.NewJSRuntime()
_ = getRegistry().Enable(runtime)
// by default import below modules every time
_ = runtime.Set("console", require.Require(runtime, console.ModuleName))
// Register embedded javacript helpers
if err := global.RegisterNativeScripts(runtime); err != nil {
gologger.Error().Msgf("Could not register scripts: %s\n", err)
}
return runtime
}
// stringify converts a given value to string
// if its a struct it will be marshalled to json
func stringify(gojaValue goja.Value, runtime *goja.Runtime) string {
value := gojaValue.Export()
if value == nil {
return ""
}
kind := reflect.TypeOf(value).Kind()
if kind == reflect.Struct || kind == reflect.Ptr && reflect.ValueOf(value).Elem().Kind() == reflect.Struct {
// in this case we must use JSON.stringify to convert to string
// because json.Marshal() utilizes json tags when marshalling
// but goja has custom implementation of json.Marshal() which does not
// since we have been using `to_json` in all our examples we must stick to it
// marshal structs or struct pointers to json automatically
jsonStringify, ok := goja.AssertFunction(runtime.Get("to_json"))
if ok {
result, err := jsonStringify(goja.Undefined(), gojaValue)
if err == nil {
return result.String()
}
}
// unlikely but if to_json throwed some error use native json.Marshal
val := value
if kind == reflect.Ptr {
val = reflect.ValueOf(value).Elem().Interface()
}
bin, err := json.Marshal(val)
if err == nil {
return string(bin)
}
}
// for everything else stringify
return fmt.Sprintf("%+v", value)
}