mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-04 06:26:51 +02:00
added script support in remote browser script
Signed-off-by: RonniSkansing <rskansing@gmail.com>
This commit is contained in:
9 files changed
+234
No files matched your search
@@ -0,0 +1,54 @@
|
||||
package script
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRunCallable proves callable mode: input flows in, the returned object
|
||||
// flows out.
|
||||
func TestRunCallable(t *testing.T) {
|
||||
r := newTestRunner()
|
||||
out, err := r.RunCallable(
|
||||
context.Background(),
|
||||
`return { doubled: input.n * 2, who: input.who };`,
|
||||
map[string]interface{}{"n": 21, "who": "alice"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if fmt.Sprint(out["doubled"]) != "42" {
|
||||
t.Fatalf("doubled = %v, want 42", out["doubled"])
|
||||
}
|
||||
if out["who"] != "alice" {
|
||||
t.Fatalf("who = %v, want alice", out["who"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunCallableStop proves stop() is a clean exit with no output.
|
||||
func TestRunCallableStop(t *testing.T) {
|
||||
r := newTestRunner()
|
||||
out, err := r.RunCallable(context.Background(), `stop();`, nil)
|
||||
if err != nil || out != nil {
|
||||
t.Fatalf("stop() should be a clean nil exit, got out=%v err=%v", out, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunCallableNonObject proves a non-object return is an error.
|
||||
func TestRunCallableNonObject(t *testing.T) {
|
||||
r := newTestRunner()
|
||||
if _, err := r.RunCallable(context.Background(), `return 5;`, nil); err == nil {
|
||||
t.Fatalf("expected an error for a non-object return")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunCallableEmitEventUnavailable proves campaign bindings are inert in
|
||||
// callable mode (emitEvent throws, surfacing as an error).
|
||||
func TestRunCallableEmitEventUnavailable(t *testing.T) {
|
||||
r := newTestRunner()
|
||||
if _, err := r.RunCallable(context.Background(),
|
||||
`emitEvent('campaign_recipient_submitted_data', {});`, nil); err == nil {
|
||||
t.Fatalf("expected emitEvent to be unavailable in callable mode")
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,9 @@ type Job struct {
|
||||
Script string
|
||||
Event EventContext
|
||||
Emit EmitFunc
|
||||
// Input is the data object for a callable run (RunCallable), exposed to the
|
||||
// script as input. Nil for campaign event triggered runs.
|
||||
Input map[string]interface{}
|
||||
|
||||
// test, when set, puts the run in capture mode: log/info/emitEvent are
|
||||
// recorded into it instead of applied, and errors are captured. Set only by
|
||||
@@ -192,6 +195,57 @@ func (r *Runner) run(job Job) {
|
||||
}
|
||||
}
|
||||
|
||||
// RunCallable runs a script in callable mode: it receives input as the `input`
|
||||
// binding, has the same http.fetch and codec toolkit, and the object it returns
|
||||
// is exported back to the caller. Campaign bindings (emitEvent, info) are inert.
|
||||
// Synchronous and bounded by the same wall clock timeout as an event run.
|
||||
func (r *Runner) RunCallable(ctx context.Context, source string, input map[string]interface{}) (out map[string]interface{}, err error) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
out = nil
|
||||
err = fmt.Errorf("script panicked: %v", rec)
|
||||
}
|
||||
}()
|
||||
timeout := r.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = DefaultTimeout
|
||||
}
|
||||
runCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
vm := goja.New()
|
||||
vm.SetMaxCallStackSize(maxCallStackSize)
|
||||
go func() {
|
||||
defer func() { _ = recover() }()
|
||||
<-runCtx.Done()
|
||||
vm.Interrupt(runCtx.Err())
|
||||
}()
|
||||
|
||||
r.registerBindings(vm, Job{Script: source, Input: input}, runCtx)
|
||||
|
||||
val, runErr := vm.RunString("(function(){\n" + source + "\n})()")
|
||||
if runErr != nil {
|
||||
var stopErr scriptStopError
|
||||
if errors.As(runErr, &stopErr) {
|
||||
return nil, nil
|
||||
}
|
||||
if _, ok := runErr.(*goja.InterruptedError); ok {
|
||||
return nil, errors.New("script exceeded its time budget")
|
||||
}
|
||||
if runCtx.Err() == context.DeadlineExceeded {
|
||||
return nil, errors.New("script exceeded its time budget")
|
||||
}
|
||||
return nil, runErr
|
||||
}
|
||||
if val == nil || goja.IsUndefined(val) || goja.IsNull(val) {
|
||||
return nil, nil
|
||||
}
|
||||
if m, ok := val.Export().(map[string]interface{}); ok {
|
||||
return m, nil
|
||||
}
|
||||
return nil, errors.New("script must return an object")
|
||||
}
|
||||
|
||||
// reportError records an uncaught script failure as a campaign info event so
|
||||
// it is visible beyond the server logs. It goes through the same event funnel as
|
||||
// emitEvent, so the detail follows the campaign's data-retention and anonymity
|
||||
@@ -245,6 +299,14 @@ func (r *Runner) registerBindings(vm *goja.Runtime, job Job, ctx context.Context
|
||||
"data": job.Event.Data,
|
||||
})
|
||||
|
||||
// input is the data object passed to a callable run (RunCallable); empty for
|
||||
// campaign event triggered runs.
|
||||
if job.Input != nil {
|
||||
vm.Set("input", job.Input)
|
||||
} else {
|
||||
vm.Set("input", map[string]interface{}{})
|
||||
}
|
||||
|
||||
vm.Set("stop", func(call goja.FunctionCall) goja.Value {
|
||||
panic(vm.NewGoError(scriptStopError{}))
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user