github.com/iDigitalFlame/xmt@v0.5.4/cmd/script/sotto/otto.go_ (about)

     1  // Copyright (C) 2020 - 2023 iDigitalFlame
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU General Public License as published by
     5  // the Free Software Foundation, either version 3 of the License, or
     6  // any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU General Public License
    14  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    15  //
    16  
    17  // Package sotto is a mapping for the Otto (github.com/robertkrimen/otto)
    18  // JavaScript engine.
    19  package sotto
    20  
    21  import (
    22  	"context"
    23  	"sync"
    24  	"time"
    25  
    26  	"github.com/iDigitalFlame/xmt/c2/task"
    27  	"github.com/iDigitalFlame/xmt/cmd"
    28  	"github.com/iDigitalFlame/xmt/util"
    29  	"github.com/robertkrimen/otto"
    30  )
    31  
    32  // Otto is a mapping for the Otto (github.com/robertkrimen/otto) JavaScript engine.
    33  // This can be used to run JavaScript directly and can be registered by the
    34  // 'task.RegisterScript' function to include the engine in the XMT task runtime.
    35  const Otto ottoEngine = 0xE0
    36  
    37  var (
    38  	ottoPool = sync.Pool{
    39  		New: func() any {
    40  			return newOtto()
    41  		},
    42  	}
    43  	ottoError = new(otto.Error)
    44  	ottoEmpty otto.Value
    45  )
    46  
    47  type ottoEngine uint8
    48  type ottoScript struct {
    49  	*otto.Otto
    50  	c util.Builder
    51  }
    52  
    53  // Register is a simple shortcut for 'task.RegisterEngine(uint8(Otto), Otto)'.
    54  func Register() error {
    55  	return task.RegisterEngine(uint8(Otto), Otto)
    56  }
    57  func newOtto() *ottoScript {
    58  	i := &ottoScript{Otto: otto.New()}
    59  	i.Interrupt = make(chan func(), 1)
    60  	if c, err := i.Get("console"); err == nil {
    61  		c.Object().Set("log", i.log)
    62  	}
    63  	i.Set("print", i.log)
    64  	i.Set("exec", exec)
    65  	i.Set("sleep", sleep)
    66  	return i
    67  }
    68  
    69  // Invoke will use the Otto (github.com/robertkrimen/otto) JavaScript engine to
    70  // run active JavaScript. This can be used to run code not built in at compile time.
    71  //
    72  // The only argument is the script that is to be run. The results are the output
    73  // of the console (all console.log together) and any errors that may occur or syntax
    74  // errors.
    75  //
    76  // This will capture the output of all the console writes and adds a 'print' statement
    77  // as a shortcut to be used.
    78  //
    79  // Another additional function 'exec' can be used to run commands natively. This
    80  // function can take a vardict of strings to be the command line arguments.
    81  func Invoke(s string) (string, error) {
    82  	return InvokeEx(context.Background(), nil, s)
    83  }
    84  func exec(v otto.FunctionCall) otto.Value {
    85  	var p cmd.Process
    86  	if len(v.ArgumentList) == 1 {
    87  		s, err := v.Argument(0).ToString()
    88  		if err != nil {
    89  			i, _ := v.Otto.ToValue(err.Error())
    90  			return i
    91  		}
    92  		p.Args = cmd.Split(s)
    93  	} else {
    94  		for i := range v.ArgumentList {
    95  			s, err := v.Argument(i).ToString()
    96  			if err != nil {
    97  				i, _ := v.Otto.ToValue(err.Error())
    98  				return i
    99  			}
   100  			p.Args = append(p.Args, s)
   101  		}
   102  	}
   103  	b, err := p.CombinedOutput()
   104  	if err != nil {
   105  		i, _ := v.Otto.ToValue(err.Error())
   106  		return i
   107  	}
   108  	if len(b) > 0 && b[len(b)-1] == 10 {
   109  		b = b[:len(b)-1]
   110  	}
   111  	i, _ := v.Otto.ToValue(string(b))
   112  	return i
   113  }
   114  func sleep(v otto.FunctionCall) otto.Value {
   115  	if len(v.ArgumentList) == 0 {
   116  		return ottoEmpty
   117  	}
   118  	n, err := v.Argument(0).ToFloat()
   119  	if err != nil {
   120  		return ottoEmpty
   121  	}
   122  	time.Sleep(time.Duration(n * float64(time.Second)))
   123  	return ottoEmpty
   124  }
   125  func (o *ottoScript) run(c chan<- error, s string) {
   126  	_, err := o.Run(s)
   127  	if err != nil && len(err.Error()) == 0 {
   128  		return
   129  	}
   130  	c <- err
   131  }
   132  func (o *ottoScript) log(v otto.FunctionCall) otto.Value {
   133  	for i := range v.ArgumentList {
   134  		if i > 0 {
   135  			o.c.WriteByte(' ')
   136  		}
   137  		o.c.WriteString(v.Argument(i).String())
   138  	}
   139  	o.c.WriteByte('\n')
   140  	return ottoEmpty
   141  }
   142  
   143  // InvokeContext will use the Otto (github.com/robertkrimen/otto) JavaScript engine
   144  // to run active JavaScript. This can be used to run code not built in at compile
   145  // time.
   146  //
   147  // A context is required to timeout the script execution and script that is to be
   148  // run. The results are the output of the console (all console.log together) and
   149  // any errors that may occur or syntax errors.
   150  //
   151  // This will capture the output of all the console writes and adds a 'print' statement
   152  // as a shortcut to be used.
   153  //
   154  // Another additional function 'exec' can be used to run commands natively. This
   155  // function can take a vardict of strings to be the command line arguments.
   156  func InvokeContext(x context.Context, s string) (string, error) {
   157  	return InvokeEx(x, nil, s)
   158  }
   159  
   160  // InvokeEx will use the Otto (github.com/robertkrimen/otto) JavaScript engine
   161  // to run active JavaScript. This can be used to run code not built in at compile
   162  // time.
   163  //
   164  // A context is required to timeout the script execution and script that is to be
   165  // run. The results are the output of the console (all console.log together) and
   166  // any errors that may occur or syntax errors.
   167  //
   168  // This will capture the output of all the console writes and adds a 'print' statement
   169  // as a shortcut to be used.
   170  //
   171  // Another additional function 'exec' can be used to run commands natively. This
   172  // function can take a vardict of strings to be the command line arguments.
   173  //
   174  // This Ex function allows to specify a map that contains any starting variables
   175  // to be supplied at runtime.
   176  func InvokeEx(x context.Context, m map[string]any, s string) (string, error) {
   177  	var (
   178  		c   = make(chan error, 1)
   179  		h   = ottoPool.Get().(*ottoScript)
   180  		err error
   181  	)
   182  	for k, v := range m {
   183  		h.Set(k, v)
   184  	}
   185  	go h.run(c, s)
   186  	select {
   187  	case <-x.Done():
   188  		h.Interrupt <- func() {
   189  			panic(ottoError)
   190  		}
   191  	case err = <-c:
   192  	}
   193  	close(c)
   194  	o := h.c.String()
   195  	h.c.Reset()
   196  	for k := range m {
   197  		h.Set(k, nil)
   198  	}
   199  	if ottoPool.Put(h); len(o) > 1 && o[len(o)-1] == '\n' {
   200  		return o[:len(o)-1], err
   201  	}
   202  	return o, err
   203  }
   204  func (ottoEngine) Invoke(x context.Context, m map[string]any, s string) (string, error) {
   205  	return InvokeEx(x, m, s)
   206  }