285 lines
6.8 KiB
Go
285 lines
6.8 KiB
Go
package jail
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/robertkrimen/otto"
|
|
"github.com/status-im/status-go/geth/common"
|
|
"github.com/status-im/status-go/geth/rpc"
|
|
"github.com/status-im/status-go/static"
|
|
)
|
|
|
|
const (
|
|
web3InstanceCode = `
|
|
var Web3 = require('web3');
|
|
var web3 = new Web3(jeth);
|
|
var Bignumber = require("bignumber.js");
|
|
function bn(val) {
|
|
return new Bignumber(val);
|
|
}
|
|
`
|
|
)
|
|
|
|
var (
|
|
web3Code = string(static.MustAsset("scripts/web3.js"))
|
|
// ErrNoRPCClient is returned when an RPC client is required but it's nil.
|
|
ErrNoRPCClient = errors.New("RPC client is not available")
|
|
)
|
|
|
|
// RPCClientProvider is an interface that provides a way
|
|
// to obtain an rpc.Client.
|
|
type RPCClientProvider interface {
|
|
RPCClient() *rpc.Client
|
|
}
|
|
|
|
// Jail manages multiple JavaScript execution contexts (JavaScript VMs) called cells.
|
|
// Each cell is a separate VM with web3.js set up.
|
|
//
|
|
// As rpc.Client might not be available during Jail initialization,
|
|
// a provider function is used.
|
|
type Jail struct {
|
|
rpcClientProvider RPCClientProvider
|
|
baseJS string
|
|
cellsMx sync.RWMutex
|
|
cells map[string]*Cell
|
|
}
|
|
|
|
// New returns a new Jail.
|
|
func New(provider RPCClientProvider) *Jail {
|
|
return NewWithBaseJS(provider, "")
|
|
}
|
|
|
|
// NewWithBaseJS returns a new Jail with base JS configured.
|
|
func NewWithBaseJS(provider RPCClientProvider, code string) *Jail {
|
|
return &Jail{
|
|
rpcClientProvider: provider,
|
|
baseJS: code,
|
|
cells: make(map[string]*Cell),
|
|
}
|
|
}
|
|
|
|
// SetBaseJS sets initial JavaScript code loaded to each new cell.
|
|
func (j *Jail) SetBaseJS(js string) {
|
|
j.baseJS = js
|
|
}
|
|
|
|
// Stop stops jail and all assosiacted cells.
|
|
func (j *Jail) Stop() {
|
|
j.cellsMx.Lock()
|
|
defer j.cellsMx.Unlock()
|
|
|
|
for _, cell := range j.cells {
|
|
cell.Stop() //nolint: errcheck
|
|
}
|
|
|
|
// TODO(tiabc): Move this initialisation to a proper place.
|
|
j.cells = make(map[string]*Cell)
|
|
}
|
|
|
|
// createCell creates a new cell if it does not exists.
|
|
func (j *Jail) createCell(chatID string) (*Cell, error) {
|
|
j.cellsMx.Lock()
|
|
defer j.cellsMx.Unlock()
|
|
|
|
if cell, ok := j.cells[chatID]; ok {
|
|
return cell, fmt.Errorf("cell with id '%s' already exists", chatID)
|
|
}
|
|
|
|
cell, err := NewCell(chatID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
j.cells[chatID] = cell
|
|
|
|
return cell, nil
|
|
}
|
|
|
|
// CreateCell creates a new cell. It returns an error
|
|
// if a cell with a given ID already exists.
|
|
func (j *Jail) CreateCell(chatID string) (common.JailCell, error) {
|
|
return j.createCell(chatID)
|
|
}
|
|
|
|
// initCell initializes a cell with default JavaScript handlers and user code.
|
|
func (j *Jail) initCell(cell *Cell) error {
|
|
// Register objects being a bridge between Go and JavaScript.
|
|
if err := registerWeb3Provider(j, cell); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := registerStatusSignals(cell); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Run some initial JS code to provide some global objects.
|
|
c := []string{
|
|
j.baseJS,
|
|
web3Code,
|
|
web3InstanceCode,
|
|
}
|
|
|
|
_, err := cell.Run(strings.Join(c, ";"))
|
|
return err
|
|
}
|
|
|
|
// CreateAndInitCell creates and initializes a new Cell.
|
|
func (j *Jail) createAndInitCell(chatID string, code ...string) (*Cell, error) {
|
|
cell, err := j.createCell(chatID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := j.initCell(cell); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Run custom user code
|
|
for _, js := range code {
|
|
_, err := cell.Run(js)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return cell, nil
|
|
}
|
|
|
|
// CreateAndInitCell creates and initializes new Cell. Additionally,
|
|
// it creates a `catalog` variable in the VM.
|
|
// It returns the response as a JSON string.
|
|
func (j *Jail) CreateAndInitCell(chatID string, code ...string) string {
|
|
cell, err := j.createAndInitCell(chatID, code...)
|
|
if err != nil {
|
|
return newJailErrorResponse(err)
|
|
}
|
|
|
|
return j.makeCatalogVariable(cell)
|
|
}
|
|
|
|
// makeCatalogVariable provides `catalog` as a global variable.
|
|
// TODO(divan): this can and should be implemented outside of jail,
|
|
// on a clojure side. Moving this into separate method to nuke it later
|
|
// easier.
|
|
func (j *Jail) makeCatalogVariable(cell *Cell) string {
|
|
_, err := cell.Run(`var catalog = JSON.stringify(_status_catalog)`)
|
|
if err != nil {
|
|
return newJailErrorResponse(err)
|
|
}
|
|
|
|
value, err := cell.Get("catalog")
|
|
if err != nil {
|
|
return newJailErrorResponse(err)
|
|
}
|
|
|
|
return newJailResultResponse(value)
|
|
}
|
|
|
|
func (j *Jail) cell(chatID string) (*Cell, error) {
|
|
j.cellsMx.RLock()
|
|
defer j.cellsMx.RUnlock()
|
|
|
|
cell, ok := j.cells[chatID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("cell '%s' not found", chatID)
|
|
}
|
|
|
|
return cell, nil
|
|
}
|
|
|
|
// Cell returns a cell by chatID. If it does not exist, error is returned.
|
|
// Required by the Backend.
|
|
func (j *Jail) Cell(chatID string) (common.JailCell, error) {
|
|
return j.cell(chatID)
|
|
}
|
|
|
|
// Execute allows to run arbitrary JS code within a cell.
|
|
func (j *Jail) Execute(chatID, code string) string {
|
|
cell, err := j.cell(chatID)
|
|
if err != nil {
|
|
return newJailErrorResponse(err)
|
|
}
|
|
|
|
value, err := cell.Run(code)
|
|
if err != nil {
|
|
return newJailErrorResponse(err)
|
|
}
|
|
|
|
return value.String()
|
|
}
|
|
|
|
// Call executes the `call` function within a cell with chatID.
|
|
// Returns a string being a valid JS code. In case of a successful result,
|
|
// it's {"result": any}. In case of an error: {"error": "some error"}.
|
|
//
|
|
// Call calls commands from `_status_catalog`.
|
|
// commandPath is an array of properties to retrieve a function.
|
|
// For instance:
|
|
// `["prop1", "prop2"]` is translated to `_status_catalog["prop1"]["prop2"]`.
|
|
func (j *Jail) Call(chatID, commandPath, args string) string {
|
|
cell, err := j.cell(chatID)
|
|
if err != nil {
|
|
return newJailErrorResponse(err)
|
|
}
|
|
|
|
value, err := cell.Call("call", nil, commandPath, args)
|
|
if err != nil {
|
|
return newJailErrorResponse(err)
|
|
}
|
|
|
|
return newJailResultResponse(value)
|
|
}
|
|
|
|
// RPCClient returns an rpc.Client.
|
|
func (j *Jail) RPCClient() *rpc.Client {
|
|
if j.rpcClientProvider == nil {
|
|
return nil
|
|
}
|
|
|
|
return j.rpcClientProvider.RPCClient()
|
|
}
|
|
|
|
// sendRPCCall executes a raw JSON-RPC request.
|
|
func (j *Jail) sendRPCCall(request string) (interface{}, error) {
|
|
client := j.RPCClient()
|
|
if client == nil {
|
|
return nil, ErrNoRPCClient
|
|
}
|
|
|
|
rawResponse := client.CallRaw(request)
|
|
|
|
var response interface{}
|
|
if err := json.Unmarshal([]byte(rawResponse), &response); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal response: %s", err)
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
// newJailErrorResponse returns an error.
|
|
func newJailErrorResponse(err error) string {
|
|
response := struct {
|
|
Error string `json:"error"`
|
|
}{
|
|
Error: err.Error(),
|
|
}
|
|
|
|
rawResponse, err := json.Marshal(response)
|
|
if err != nil {
|
|
return `{"error": "` + err.Error() + `"}`
|
|
}
|
|
|
|
return string(rawResponse)
|
|
}
|
|
|
|
// newJailResultResponse returns a string that is a valid JavaScript code.
|
|
// Marshaling is not required as result.String() produces a string
|
|
// that is a valid JavaScript code.
|
|
func newJailResultResponse(result otto.Value) string {
|
|
return `{"result": ` + result.String() + `}`
|
|
}
|