console: use goja in the bridge

This commit is contained in:
Guillaume Ballet 2019-12-18 18:39:32 +01:00
parent 694d678641
commit 498eaed1de
2 changed files with 180 additions and 128 deletions

View file

@ -20,37 +20,58 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"reflect"
"strings" "strings"
"time" "time"
"github.com/dop251/goja"
"github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/scwallet"
"github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/robertkrimen/otto"
) )
// bridge is a collection of JavaScript utility methods to bride the .js runtime // bridge is a collection of JavaScript utility methods to bride the .js runtime
// environment and the Go RPC connection backing the remote method calls. // environment and the Go RPC connection backing the remote method calls.
type bridge struct { type bridge struct {
client *rpc.Client // RPC client to execute Ethereum requests through client *rpc.Client // RPC client to execute Ethereum requests through
prompter UserPrompter // Input prompter to allow interactive user feedback prompter UserPrompter // Input prompter to allow interactive user feedback
printer io.Writer // Output writer to serialize any display strings to printer io.Writer // Output writer to serialize any display strings to
runtime *goja.Runtime // Pointer to the JS runtime
} }
// newBridge creates a new JavaScript wrapper around an RPC client. // newBridge creates a new JavaScript wrapper around an RPC client.
func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *bridge { func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer, runtime *goja.Runtime) *bridge {
return &bridge{ return &bridge{
client: client, client: client,
prompter: prompter, prompter: prompter,
printer: printer, printer: printer,
runtime: runtime,
} }
} }
// IsNumber returns `true` if input value `v` is a number.
func IsNumber(v goja.Value) bool {
switch v.ExportType().Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return true
default:
return false
}
}
func getJeth(r *goja.Runtime) *goja.Object {
jethObj := r.Get("jeth")
if jethObj == nil {
panic(r.ToValue("jeth object does not exist"))
}
return jethObj.ToObject(r)
}
// NewAccount is a wrapper around the personal.newAccount RPC method that uses a // NewAccount is a wrapper around the personal.newAccount RPC method that uses a
// non-echoing password prompt to acquire the passphrase and executes the original // non-echoing password prompt to acquire the passphrase and executes the original
// RPC method (saved in jeth.newAccount) with it to actually execute the RPC call. // RPC method (saved in jeth.newAccount) with it to actually execute the RPC call.
func (b *bridge) NewAccount(call otto.FunctionCall) (response otto.Value) { func (b *bridge) NewAccount(call goja.FunctionCall) (response goja.Value) {
var ( var (
password string password string
confirm string confirm string
@ -58,50 +79,58 @@ func (b *bridge) NewAccount(call otto.FunctionCall) (response otto.Value) {
) )
switch { switch {
// No password was specified, prompt the user for it // No password was specified, prompt the user for it
case len(call.ArgumentList) == 0: case len(call.Arguments) == 0:
if password, err = b.prompter.PromptPassword("Password: "); err != nil { if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
if confirm, err = b.prompter.PromptPassword("Repeat password: "); err != nil { if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
if password != confirm { if password != confirm {
throwJSException("passwords don't match!") throwJSException(b.runtime, "passwords don't match!")
} }
// A single string password was specified, use that // A single string password was specified, use that
case len(call.ArgumentList) == 1 && call.Argument(0).IsString(): case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil:
password, _ = call.Argument(0).ToString() password = call.Argument(0).ToString().String()
// Otherwise fail with some error // Otherwise fail with some error
default: default:
throwJSException("expected 0 or 1 string argument") throwJSException(b.runtime, "expected 0 or 1 string argument")
} }
// Password acquired, execute the call and return // Password acquired, execute the call and return
ret, err := call.Otto.Call("jeth.newAccount", nil, password) newAccount, callable := goja.AssertFunction(getJeth(b.runtime).Get("newAccount"))
if !callable {
panic(b.runtime.ToValue("jeth.newAccount isn't callable"))
}
ret, err := newAccount(goja.Null(), b.runtime.ToValue(password))
if err != nil { if err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
return ret return ret
} }
// OpenWallet is a wrapper around personal.openWallet which can interpret and // OpenWallet is a wrapper around personal.openWallet which can interpret and
// react to certain error messages, such as the Trezor PIN matrix request. // react to certain error messages, such as the Trezor PIN matrix request.
func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) { func (b *bridge) OpenWallet(call goja.FunctionCall) (response goja.Value) {
// Make sure we have a wallet specified to open // Make sure we have a wallet specified to open
if !call.Argument(0).IsString() { if call.Argument(0).ToObject(b.runtime).ClassName() != "String" {
throwJSException("first argument must be the wallet URL to open") throwJSException(b.runtime, b.runtime.ToValue("first argument must be the wallet URL to open"))
} }
wallet := call.Argument(0) wallet := call.Argument(0)
var passwd otto.Value var passwd goja.Value
if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() { if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
passwd, _ = otto.ToValue("") passwd = b.runtime.ToValue("")
} else { } else {
passwd = call.Argument(1) passwd = call.Argument(1)
} }
// Open the wallet and return if successful in itself // Open the wallet and return if successful in itself
val, err := call.Otto.Call("jeth.openWallet", nil, wallet, passwd) openWallet, callable := goja.AssertFunction(getJeth(b.runtime).Get("openWallet"))
if !callable {
throwJSException(b.runtime, b.runtime.ToValue("jeth.openWallet is not callable"))
}
val, err := openWallet(goja.Null(), wallet, passwd)
if err == nil { if err == nil {
return val return val
} }
@ -115,28 +144,28 @@ func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) {
} }
val, err = b.readPassphraseAndReopenWallet(call) val, err = b.readPassphraseAndReopenWallet(call)
if err != nil { if err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()): case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()):
// PUK input requested, fetch from the user and call open again // PUK input requested, fetch from the user and call open again
if input, err := b.prompter.PromptPassword("Please enter the pairing password: "); err != nil { if input, err := b.prompter.PromptPassword("Please enter the pairing password: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} else { } else {
passwd, _ = otto.ToValue(input) passwd = b.runtime.ToValue(input)
} }
if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { if val, err = openWallet(goja.Null(), wallet, passwd); err != nil {
if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) { if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} else { } else {
// PIN input requested, fetch from the user and call open again // PIN input requested, fetch from the user and call open again
if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} else { } else {
passwd, _ = otto.ToValue(input) passwd = b.runtime.ToValue(input)
} }
if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { if val, err = openWallet(goja.Null(), wallet, passwd); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
} }
} }
@ -145,51 +174,55 @@ func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) {
// PIN unblock requested, fetch PUK and new PIN from the user // PIN unblock requested, fetch PUK and new PIN from the user
var pukpin string var pukpin string
if input, err := b.prompter.PromptPassword("Please enter current PUK: "); err != nil { if input, err := b.prompter.PromptPassword("Please enter current PUK: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} else { } else {
pukpin = input pukpin = input
} }
if input, err := b.prompter.PromptPassword("Please enter new PIN: "); err != nil { if input, err := b.prompter.PromptPassword("Please enter new PIN: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} else { } else {
pukpin += input pukpin += input
} }
passwd, _ = otto.ToValue(pukpin) passwd = b.runtime.ToValue(pukpin)
if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { if val, err = openWallet(goja.Null(), wallet, passwd); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()): case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()):
// PIN input requested, fetch from the user and call open again // PIN input requested, fetch from the user and call open again
if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} else { } else {
passwd, _ = otto.ToValue(input) passwd = b.runtime.ToValue(input)
} }
if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { if val, err = openWallet(goja.Null(), wallet, passwd); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
default: default:
// Unknown error occurred, drop to the user // Unknown error occurred, drop to the user
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
return val return val
} }
func (b *bridge) readPassphraseAndReopenWallet(call otto.FunctionCall) (otto.Value, error) { func (b *bridge) readPassphraseAndReopenWallet(call goja.FunctionCall) (goja.Value, error) {
var passwd otto.Value var passwd goja.Value
wallet := call.Argument(0) wallet := call.Argument(0)
if input, err := b.prompter.PromptPassword("Please enter your password: "); err != nil { if input, err := b.prompter.PromptPassword("Please enter your passphrase: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} else { } else {
passwd, _ = otto.ToValue(input) passwd = b.runtime.ToValue(input)
} }
return call.Otto.Call("jeth.openWallet", nil, wallet, passwd) openWallet, callable := goja.AssertFunction(getJeth(b.runtime).Get("openWallet"))
if !callable {
return nil, fmt.Errorf("jeth.openWallet is not callable")
}
return openWallet(goja.Null(), wallet, passwd)
} }
func (b *bridge) readPinAndReopenWallet(call otto.FunctionCall) (otto.Value, error) { func (b *bridge) readPinAndReopenWallet(call goja.FunctionCall) (goja.Value, error) {
var passwd otto.Value var passwd goja.Value
wallet := call.Argument(0) wallet := call.Argument(0)
// Trezor PIN matrix input requested, display the matrix to the user and fetch the data // Trezor PIN matrix input requested, display the matrix to the user and fetch the data
fmt.Fprintf(b.printer, "Look at the device for number positions\n\n") fmt.Fprintf(b.printer, "Look at the device for number positions\n\n")
@ -200,52 +233,60 @@ func (b *bridge) readPinAndReopenWallet(call otto.FunctionCall) (otto.Value, err
fmt.Fprintf(b.printer, "1 | 2 | 3\n\n") fmt.Fprintf(b.printer, "1 | 2 | 3\n\n")
if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} else { } else {
passwd, _ = otto.ToValue(input) passwd = b.runtime.ToValue(input)
} }
return call.Otto.Call("jeth.openWallet", nil, wallet, passwd) openWallet, callable := goja.AssertFunction(getJeth(b.runtime).Get("openWallet"))
if !callable {
return nil, fmt.Errorf("jeth.openWallet is not callable")
}
return openWallet(goja.Null(), wallet, passwd)
} }
// UnlockAccount is a wrapper around the personal.unlockAccount RPC method that // UnlockAccount is a wrapper around the personal.unlockAccount RPC method that
// uses a non-echoing password prompt to acquire the passphrase and executes the // uses a non-echoing password prompt to acquire the passphrase and executes the
// original RPC method (saved in jeth.unlockAccount) with it to actually execute // original RPC method (saved in jeth.unlockAccount) with it to actually execute
// the RPC call. // the RPC call.
func (b *bridge) UnlockAccount(call otto.FunctionCall) (response otto.Value) { func (b *bridge) UnlockAccount(call goja.FunctionCall) (response goja.Value) {
// Make sure we have an account specified to unlock // Make sure we have an account specified to unlock
if !call.Argument(0).IsString() { if call.Argument(0).ExportType().Kind() != reflect.String {
throwJSException("first argument must be the account to unlock") throwJSException(b.runtime, "first argument must be the account to unlock")
} }
account := call.Argument(0) account := call.Argument(0)
// If password is not given or is the null value, prompt the user for it // If password is not given or is the null value, prompt the user for it
var passwd otto.Value var passwd goja.Value
if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() { if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
fmt.Fprintf(b.printer, "Unlock account %s\n", account) fmt.Fprintf(b.printer, "Unlock account %s\n", account)
if input, err := b.prompter.PromptPassword("Password: "); err != nil { if input, err := b.prompter.PromptPassword("Passphrase: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} else { } else {
passwd, _ = otto.ToValue(input) passwd = b.runtime.ToValue(input)
} }
} else { } else {
if !call.Argument(1).IsString() { if call.Argument(1).ExportType().Kind() != reflect.String {
throwJSException("password must be a string") throwJSException(b.runtime, "password must be a string")
} }
passwd = call.Argument(1) passwd = call.Argument(1)
} }
// Third argument is the duration how long the account must be unlocked. // Third argument is the duration how long the account must be unlocked.
duration := otto.NullValue() duration := goja.Null()
if call.Argument(2).IsDefined() && !call.Argument(2).IsNull() { if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) {
if !call.Argument(2).IsNumber() { if !IsNumber(call.Argument(2)) {
throwJSException("unlock duration must be a number") throwJSException(b.runtime, "unlock duration must be a number")
} }
duration = call.Argument(2) duration = call.Argument(2)
} }
// Send the request to the backend and return // Send the request to the backend and return
val, err := call.Otto.Call("jeth.unlockAccount", nil, account, passwd, duration) unlockAccount, callable := goja.AssertFunction(getJeth(b.runtime).Get("unlockAccount"))
if !callable {
throwJSException(b.runtime, "jeth.unlockAccount is not callable")
}
val, err := unlockAccount(goja.Null(), account, passwd, duration)
if err != nil { if err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
return val return val
} }
@ -253,89 +294,98 @@ func (b *bridge) UnlockAccount(call otto.FunctionCall) (response otto.Value) {
// Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password // Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password
// prompt to acquire the passphrase and executes the original RPC method (saved in // prompt to acquire the passphrase and executes the original RPC method (saved in
// jeth.sign) with it to actually execute the RPC call. // jeth.sign) with it to actually execute the RPC call.
func (b *bridge) Sign(call otto.FunctionCall) (response otto.Value) { func (b *bridge) Sign(call goja.FunctionCall) (response goja.Value) {
var ( var (
message = call.Argument(0) message = call.Argument(0)
account = call.Argument(1) account = call.Argument(1)
passwd = call.Argument(2) passwd = call.Argument(2)
) )
if !message.IsString() { if message.ExportType().Kind() != reflect.String {
throwJSException("first argument must be the message to sign") throwJSException(b.runtime, "first argument must be the message to sign")
} }
if !account.IsString() { if account.ExportType().Kind() != reflect.String {
throwJSException("second argument must be the account to sign with") throwJSException(b.runtime, "second argument must be the account to sign with")
} }
// if the password is not given or null ask the user and ensure password is a string // if the password is not given or null ask the user and ensure password is a string
if passwd.IsUndefined() || passwd.IsNull() { if goja.IsUndefined(passwd) || goja.IsNull(passwd) {
fmt.Fprintf(b.printer, "Give password for account %s\n", account) fmt.Fprintf(b.printer, "Give password for account %s\n", account)
if input, err := b.prompter.PromptPassword("Password: "); err != nil { if input, err := b.prompter.PromptPassword("Password: "); err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} else { } else {
passwd, _ = otto.ToValue(input) passwd = b.runtime.ToValue(input)
} }
} }
if !passwd.IsString() { if passwd.ExportType().Kind() != reflect.String {
throwJSException("third argument must be the password to unlock the account") throwJSException(b.runtime, "third argument must be the password to unlock the account")
} }
// Send the request to the backend and return // Send the request to the backend and return
val, err := call.Otto.Call("jeth.sign", nil, message, account, passwd) sign, callable := goja.AssertFunction(getJeth(b.runtime).Get("unlockAccount"))
if !callable {
throwJSException(b.runtime, "jeth.unlockAccount is not callable")
}
val, err := sign(goja.Null(), message, account, passwd)
if err != nil { if err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
return val return val
} }
// Sleep will block the console for the specified number of seconds. // Sleep will block the console for the specified number of seconds.
func (b *bridge) Sleep(call otto.FunctionCall) (response otto.Value) { func (b *bridge) Sleep(call goja.FunctionCall) (response goja.Value) {
if call.Argument(0).IsNumber() { if IsNumber(call.Argument(0)) {
sleep, _ := call.Argument(0).ToInteger() sleep := call.Argument(0).ToInteger()
time.Sleep(time.Duration(sleep) * time.Second) time.Sleep(time.Duration(sleep) * time.Second)
return otto.TrueValue() return b.runtime.ToValue(true)
} }
return throwJSException("usage: sleep(<number of seconds>)") return throwJSException(b.runtime, "usage: sleep(<number of seconds>)")
} }
// SleepBlocks will block the console for a specified number of new blocks optionally // SleepBlocks will block the console for a specified number of new blocks optionally
// until the given timeout is reached. // until the given timeout is reached.
func (b *bridge) SleepBlocks(call otto.FunctionCall) (response otto.Value) { func (b *bridge) SleepBlocks(call goja.FunctionCall) (response goja.Value) {
var ( var (
blocks = int64(0) blocks = int64(0)
sleep = int64(9999999999999999) // indefinitely sleep = int64(9999999999999999) // indefinitely
) )
// Parse the input parameters for the sleep // Parse the input parameters for the sleep
nArgs := len(call.ArgumentList) nArgs := len(call.Arguments)
if nArgs == 0 { if nArgs == 0 {
throwJSException("usage: sleepBlocks(<n blocks>[, max sleep in seconds])") throwJSException(b.runtime, "usage: sleepBlocks(<n blocks>[, max sleep in seconds])")
} }
if nArgs >= 1 { if nArgs >= 1 {
if call.Argument(0).IsNumber() { if IsNumber(call.Argument(0)) {
blocks, _ = call.Argument(0).ToInteger() blocks = call.Argument(0).ToInteger()
} else { } else {
throwJSException("expected number as first argument") throwJSException(b.runtime, "expected number as first argument")
} }
} }
if nArgs >= 2 { if nArgs >= 2 {
if call.Argument(1).IsNumber() { if IsNumber(call.Argument(1)) {
sleep, _ = call.Argument(1).ToInteger() sleep = call.Argument(1).ToInteger()
} else { } else {
throwJSException("expected number as second argument") throwJSException(b.runtime, "expected number as second argument")
} }
} }
// go through the console, this will allow web3 to call the appropriate // go through the console, this will allow web3 to call the appropriate
// callbacks if a delayed response or notification is received. // callbacks if a delayed response or notification is received.
blockNumber := func() int64 { blockNumber := func() int64 {
result, err := call.Otto.Run("eth.blockNumber") blockNumber, isFunc := goja.AssertFunction(b.runtime.Get("eth.blockNumber"))
if err != nil { if !isFunc {
throwJSException(err.Error()) throwJSException(b.runtime, "eth.blockNumber isn't a function")
} }
block, err := result.ToInteger() block, err := blockNumber(goja.Null())
if err != nil { if err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
return block // XXX This will return 0 if blockNumber isn't an Integer. This is
// actually consistent with the current behavior (block number is 0
// until the sync is done) but not safe enough.
return block.ToInteger()
} }
// Poll the current block number until either it ot a timeout is reached // Poll the current block number until either it ot a timeout is reached
targetBlockNr := blockNumber() + blocks targetBlockNr := blockNumber() + blocks
@ -343,11 +393,11 @@ func (b *bridge) SleepBlocks(call otto.FunctionCall) (response otto.Value) {
for time.Now().Before(deadline) { for time.Now().Before(deadline) {
if blockNumber() >= targetBlockNr { if blockNumber() >= targetBlockNr {
return otto.TrueValue() return b.runtime.ToValue(true)
} }
time.Sleep(time.Second) time.Sleep(time.Second)
} }
return otto.FalseValue() return b.runtime.ToValue(false)
} }
type jsonrpcCall struct { type jsonrpcCall struct {
@ -357,15 +407,14 @@ type jsonrpcCall struct {
} }
// Send implements the web3 provider "send" method. // Send implements the web3 provider "send" method.
func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) { func (b *bridge) Send(call goja.FunctionCall) (response goja.Value) {
// Remarshal the request into a Go value. // Remarshal the request into a Go value.
JSON, _ := call.Otto.Object("JSON") reqVal, err := call.Argument(0).ToObject(b.runtime).MarshalJSON()
reqVal, err := JSON.Call("stringify", call.Argument(0))
if err != nil { if err != nil {
throwJSException(err.Error()) throwJSException(b.runtime, err.Error())
} }
var ( var (
rawReq = reqVal.String() rawReq = string(reqVal)
dec = json.NewDecoder(strings.NewReader(rawReq)) dec = json.NewDecoder(strings.NewReader(rawReq))
reqs []jsonrpcCall reqs []jsonrpcCall
batch bool batch bool
@ -381,9 +430,10 @@ func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
} }
// Execute the requests. // Execute the requests.
resps, _ := call.Otto.Object("new Array()") var resps []*goja.Object
for _, req := range reqs { for _, req := range reqs {
resp, _ := call.Otto.Object(`({"jsonrpc":"2.0"})`) v, _ := b.runtime.RunString(`({"jsonrpc":"2.0"})`)
resp := v.ToObject(b.runtime)
resp.Set("id", req.ID) resp.Set("id", req.ID)
var result json.RawMessage var result json.RawMessage
err = b.client.Call(&result, req.Method, req.Params...) err = b.client.Call(&result, req.Method, req.Params...)
@ -392,9 +442,15 @@ func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
if result == nil { if result == nil {
// Special case null because it is decoded as an empty // Special case null because it is decoded as an empty
// raw message for some reason. // raw message for some reason.
resp.Set("result", otto.NullValue()) resp.Set("result", goja.Null())
} else { } else {
resultVal, err := JSON.Call("parse", string(result)) JSON := b.runtime.Get("JSON").ToObject(b.runtime)
parse, callable := goja.AssertFunction(JSON.Get("parse"))
if !callable {
panic("JSON.parse isn't a function")
}
resultVal, err := parse(goja.Null(), b.runtime.ToValue(string(result)))
if err != nil { if err != nil {
setError(resp, -32603, err.Error()) setError(resp, -32603, err.Error())
} else { } else {
@ -406,33 +462,29 @@ func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
default: default:
setError(resp, -32603, err.Error()) setError(resp, -32603, err.Error())
} }
resps.Call("push", resp) resps = append(resps, resp)
} }
// Return the responses either to the callback (if supplied) // Return the responses either to the callback (if supplied)
// or directly as the return value. // or directly as the return value.
if batch { if batch {
response = resps.Value() response = b.runtime.ToValue(resps)
} else { } else {
response, _ = resps.Get("0") response = resps[0]
} }
if fn := call.Argument(1); fn.Class() == "Function" { if fn, isFunc := goja.AssertFunction(call.Argument(1)); isFunc {
fn.Call(otto.NullValue(), otto.NullValue(), response) fn(goja.Null(), goja.Null(), response)
return otto.UndefinedValue() return goja.Undefined()
} }
return response return response
} }
func setError(resp *otto.Object, code int, msg string) { func setError(resp *goja.Object, code int, msg string) {
resp.Set("error", map[string]interface{}{"code": code, "message": msg}) resp.Set("error", map[string]interface{}{"code": code, "message": msg})
} }
// throwJSException panics on an otto.Value. The Otto VM will recover from the // throwJSException panics on an goja.Value. The Goja VM will recover from the
// Go panic and throw msg as a JavaScript error. // Go panic and throw msg as a JavaScript error.
func throwJSException(msg interface{}) otto.Value { func throwJSException(runtime *goja.Runtime, msg interface{}) goja.Value {
val, err := otto.ToValue(msg) panic(runtime.ToValue(msg))
if err != nil {
log.Error("Failed to serialize JavaScript exception", "exception", msg, "err", err)
}
panic(val)
} }

2
go.mod
View file

@ -18,7 +18,7 @@ require (
github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea
github.com/dlclark/regexp2 v1.2.0 // indirect github.com/dlclark/regexp2 v1.2.0 // indirect
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf
github.com/dop251/goja v0.0.0-20191203121440-007eef3bc40f // indirect github.com/dop251/goja v0.0.0-20191203121440-007eef3bc40f
github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c
github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa
github.com/fatih/color v1.3.0 github.com/fatih/color v1.3.0