diff --git a/wasm/bus_bridge.go b/wasm/bus_bridge.go index d2709e3..f3bd751 100644 --- a/wasm/bus_bridge.go +++ b/wasm/bus_bridge.go @@ -1,4 +1,5 @@ //go:build js && wasm +// +build js,wasm // Copyright 2021 VMware, Inc. // SPDX-License-Identifier: BSD-2-Clause @@ -6,6 +7,7 @@ package wasm import ( + "reflect" "syscall/js" "github.com/google/uuid" @@ -24,10 +26,8 @@ func NewTransportWasmBridge(busRef bus.EventBus) *TransportWasmBridge { } func (t *TransportWasmBridge) sendRequestMessageWrapper() js.Func { - jsFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { - // release the resources allocated for js.Func once this closure goes out of scope - defer jsFunc.Release() - + var jsFunc js.Func + jsFunc = js.FuncOf(func(this js.Value, args []js.Value) interface{} { var destId *uuid.UUID channel := args[0].String() payload := args[1].JSValue() @@ -47,10 +47,8 @@ func (t *TransportWasmBridge) sendRequestMessageWrapper() js.Func { } func (t *TransportWasmBridge) sendResponseMessageWrapper() js.Func { - jsFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { - // release the resources allocated for js.Func once this closure goes out of scope - defer jsFunc.Release() - + var jsFunc js.Func + jsFunc = js.FuncOf(func(this js.Value, args []js.Value) interface{} { var destId *uuid.UUID channel := args[0].String() payload := args[1].JSValue() @@ -70,13 +68,16 @@ func (t *TransportWasmBridge) sendResponseMessageWrapper() js.Func { } func (t *TransportWasmBridge) listenStreamWrapper() js.Func { - jsFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { + var jsFunc js.Func + jsFunc = js.FuncOf(func(this js.Value, args []js.Value) interface{} { handler, err := t.busRef.ListenStream(args[0].String()) if err != nil { errCtor := js.Global().Get("Error") - return errCtor.New(err.Error()) + js.Global().Get("console").Call("error", errCtor.New(err.Error())) + return nil } + jsFuncRefs := make([]js.Func, 0) closerFuncRef := js.FuncOf(func(this js.Value, args []js.Value) interface{} { handler.Close() @@ -89,18 +90,19 @@ func (t *TransportWasmBridge) listenStreamWrapper() js.Func { }) // store js.Func handles for resource cleanup later - jsFuncRefs := []js.Func{jsFunc, getResponseHandlerJsCallbackFunc(handler), closerFuncRef} + jsFuncRefs = append(jsFuncRefs, getResponseHandlerJsCallbackFunc(handler), closerFuncRef) return map[string]interface{}{ - "handle": jsFuncRefs[1], - "close": jsFuncRefs[2], + "handle": jsFuncRefs[0], + "close": jsFuncRefs[1], } }) return jsFunc } func (t *TransportWasmBridge) listenStreamWithIdWrapper() js.Func { - jsFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { + var jsFunc js.Func + jsFunc = js.FuncOf(func(this js.Value, args []js.Value) interface{} { if len(args) <= 1 { errCtor := js.Global().Get("Error") js.Global().Get("console").Call("error", errCtor.New("cannot listen to channel. missing destination ID")) @@ -119,6 +121,7 @@ func (t *TransportWasmBridge) listenStreamWithIdWrapper() js.Func { return errCtor.New(err.Error()) } + jsFuncRefs := make([]js.Func, 0) closerFuncRef := js.FuncOf(func(this js.Value, args []js.Value) interface{} { handler.Close() @@ -131,13 +134,14 @@ func (t *TransportWasmBridge) listenStreamWithIdWrapper() js.Func { }) // store js.Func handles for resource cleanup later - jsFuncRefs := []js.Func{jsFunc, getResponseHandlerJsCallbackFunc(handler), closerFuncRef} + jsFuncRefs = append(jsFuncRefs, getResponseHandlerJsCallbackFunc(handler), closerFuncRef) return map[string]interface{}{ - "handle": jsFuncRefs[1], - "close": jsFuncRefs[2], + "handle": jsFuncRefs[0], + "close": jsFuncRefs[1], } }) + return jsFunc } func (t *TransportWasmBridge) browserBusWrapper() js.Func { @@ -167,20 +171,54 @@ func (t *TransportWasmBridge) SetUpTransportWasmAPI() { func getResponseHandlerJsCallbackFunc(handler bus.MessageHandler) js.Func { return js.FuncOf(func(this js.Value, args []js.Value) interface{} { + successCallback := args[0] + hasFailureCallback := len(args) > 1 + + if (successCallback.Type() != js.TypeFunction) || (hasFailureCallback && args[1].Type() != js.TypeFunction) { + errCtor := js.Global().Get("Error") + js.Global().Get("console").Call("error", errCtor.New("invalid argument. not a function.")) + handler.Close() + return nil + } + handler.Handle(func(m *model.Message) { - jsCallback := args[0] - if jsCallback.Type() != js.TypeFunction { - errCtor := js.Global().Get("Error") - js.Global().Get("console").Call("error", errCtor.New("invalid argument. not a function.")) - handler.Close() - return + // workaround for js.ValueOf() panicking for slices of primitive types such as []int, []string, []bool, or + // a map containing such containers for their keys. this is quite dirty but hopefully when generics arrive + // we should be able to handle this situation more elegantly & hope go team improves js package. + var payload = m.Payload + switch payload.(type) { + case []string, []int, []bool, []float64, map[string]interface{}: + payload = toWasmSafePayload(payload) } - jsCallback.Invoke(m.Payload) + successCallback.Invoke(payload) }, func(e error) { - errCtor := js.Global().Get("Error") - js.Global().Get("console").Call("error", errCtor.New(e.Error())) - handler.Close() + if hasFailureCallback { + args[1].Invoke(e.Error()) + } else { + errCtor := js.Global().Get("Error") + js.Global().Get("console").Call("error", errCtor.New(e.Error())) + } }) return nil }) } + +func toWasmSafePayload(payload interface{}) interface{} { + refl := reflect.ValueOf(payload) + if refl.Kind() == reflect.Map { + for _, keyV := range refl.MapKeys() { + valV := refl.MapIndex(keyV) + refl.SetMapIndex(keyV, reflect.ValueOf(toWasmSafePayload(valV.Interface()))) + } + return payload + } else if refl.Kind() == reflect.Slice || refl.Kind() == reflect.Array { + var converted = make([]interface{}, 0) + for i := 0; i < refl.Len(); i++ { + converted = append(converted, refl.Index(i).Interface()) + } + return converted + } + + // let js.ValueOf() handle the rest + return payload +} diff --git a/wasm/example/sample_wasm_app/build.go b/wasm/example/sample_wasm_app/build.go index a735c43..8b27d56 100644 --- a/wasm/example/sample_wasm_app/build.go +++ b/wasm/example/sample_wasm_app/build.go @@ -1,3 +1,3 @@ -package sample_wasm_app +package main //go:generate bash -c "GOARCH=wasm; GOOS=js; go build -o ${OUT_DIR}/main.wasm main.go" diff --git a/wasm/example/sample_wasm_app/main.go b/wasm/example/sample_wasm_app/main.go index 2b913b2..2d07f72 100644 --- a/wasm/example/sample_wasm_app/main.go +++ b/wasm/example/sample_wasm_app/main.go @@ -7,11 +7,16 @@ package main import ( + "crypto/x509" + "encoding/pem" + "errors" "fmt" + "github.com/vmware/transport-go/model" + "github.com/vmware/transport-go/wasm" + "syscall/js" "time" "github.com/vmware/transport-go/bus" - "github.com/vmware/transport-go/wasm" ) func init() { @@ -20,6 +25,8 @@ func init() { cm.CreateChannel("goBusChan") cm.CreateChannel("broadcast") + NewX509CertParser() + ticker := time.NewTicker(1 * time.Second) go func() { for { @@ -32,6 +39,72 @@ func init() { }() } +type X509CertParser struct { + requestChannel string + responseChannel string +} + +func NewX509CertParser() *X509CertParser { + p := &X509CertParser{ + requestChannel: "x509-cert-parser-req", + responseChannel: "x509-cert-parser-resp", + } + + // set up bus channels for receiving requests and sending responses. + busInstance := bus.GetBus() + cm := busInstance.GetChannelManager() + cm.CreateChannel(p.requestChannel) + cm.CreateChannel(p.responseChannel) + + // set up a channel handler to accept and process requests from JS. + // once parsed or an error occurs, the response will be sent down the responseChannel. + handler, _ := busInstance.ListenRequestStream(p.requestChannel) + handler.Handle(func(message *model.Message) { + jsVal := message.Payload.(js.Value) + cert, err := p.parseFromJsValueString(jsVal) + if err != nil { + _ = busInstance.SendErrorMessage(p.responseChannel, err, nil) + return + } + + response := map[string]interface{}{ + "issuer": cert.Issuer.String(), + "dnsNames": cert.DNSNames, + "notBefore": cert.NotBefore.String(), + "notAfter": cert.NotAfter.String(), + "isCA": cert.IsCA, + "version": cert.Version, + } + // successful parse results are sent to the response channel for the success handler in the client + // to be invoked with. + _ = busInstance.SendResponseMessage(p.responseChannel, response, nil) + }, func(err error) { + // failed parse results will be sent to the same channel as well, but the error handler will be invoked instead + // in the client. + _ = busInstance.SendErrorMessage(p.responseChannel, err, nil) + }) + + return p +} + +func (p *X509CertParser) parseFromJsValueString(input js.Value) (*x509.Certificate, error) { + var b = []byte(input.String()) + var cert *x509.Certificate + var err error + + block, _ := pem.Decode(b) + if block == nil { + return nil, errors.New("invalid pem format") + } + + switch block.Type { + case "CERTIFICATE": + cert, err = x509.ParseCertificate(block.Bytes) + } + + return cert, err +} + func main() { twa := wasm.NewTransportWasmBridge(bus.GetBus()) twa.SetUpTransportWasmAPI()