diff --git a/Makefile b/Makefile index 12a1dcf..0c625cf 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ list: @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | xargs proto: protoc --go_out=. api/pb/*.proto + protoc --go_out=. dapp/registry/pb/*.proto deps: go get github.com/whyrusleeping/gx go get github.com/whyrusleeping/gx-go diff --git a/api/api.go b/api/api.go index 0fc5fc8..f36c38f 100644 --- a/api/api.go +++ b/api/api.go @@ -21,9 +21,9 @@ type UpStream interface { // Create new api with given client func New(client UpStream) *API { return &API{ - lock: sync.Mutex{}, - requests: map[string]chan*Response{}, - client: client, + lock: sync.Mutex{}, + requests: map[string]chan *Response{}, + client: client, } } diff --git a/api/api_test.go b/api/api_test.go index 813981d..650b2f9 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -3,11 +3,10 @@ package api import ( "testing" "time" - + pb "github.com/Bit-Nation/panthalassa/api/pb" - require "github.com/stretchr/testify/require" proto "github.com/golang/protobuf/proto" - + require "github.com/stretchr/testify/require" ) type testUpStream struct { @@ -23,59 +22,59 @@ func TestAPI_addAndCutRequestWorks(t *testing.T) { req := pb.Request{} req.RequestID = "hi" - + // api api := New(&testUpStream{}) - + // make sure request doesn't exist _, exist := api.requests["hi"] require.False(t, exist) - + api.addRequest(&req) - + // make sure request does exist _, exist = api.requests["hi"] require.True(t, exist) - + // now cut request our of the stack and make sure it was removed api.cutRequest("hi") _, exist = api.requests["hi"] require.False(t, exist) - + } -func TestRequestResponse(t *testing.T) { - +func TestRequestResponse(t *testing.T) { + dataChan := make(chan string) - + var receivedRequestID string - + // api api := New(&testUpStream{ sendFn: func(data string) { dataChan <- data }, }) - + go func() { select { - case data := <-dataChan: - req := &pb.Request{} - if err := proto.Unmarshal([]byte(data), req); err != nil { - panic(err) - } - receivedRequestID = req.RequestID - out := api.Respond(req.RequestID, &pb.Response{ - RequestID: req.RequestID, - }, nil, time.Second) - if out != nil { - panic("expected nil but got: " + out.Error()) - } + case data := <-dataChan: + req := &pb.Request{} + if err := proto.Unmarshal([]byte(data), req); err != nil { + panic(err) + } + receivedRequestID = req.RequestID + out := api.Respond(req.RequestID, &pb.Response{ + RequestID: req.RequestID, + }, nil, time.Second) + if out != nil { + panic("expected nil but got: " + out.Error()) + } } }() - + resp, err := api.request(&pb.Request{}, time.Second) require.Nil(t, err) require.Equal(t, resp.Msg.RequestID, receivedRequestID) - -} \ No newline at end of file + +} diff --git a/api/doc.go b/api/doc.go index ca1a322..bb3f7c4 100644 --- a/api/doc.go +++ b/api/doc.go @@ -10,4 +10,4 @@ package api // to the request and response protobufs // you can then extend the api struct // if you e.g. would like to implement the DHT CRUD you can create a new file -// called `dht.go` in the api folder and start to implement the requests \ No newline at end of file +// called `dht.go` in the api folder and start to implement the requests diff --git a/crypto/aes/utils_test.go b/crypto/aes/utils_test.go index 98caafb..d5815ab 100644 --- a/crypto/aes/utils_test.go +++ b/crypto/aes/utils_test.go @@ -3,9 +3,10 @@ package aes import ( "crypto/hmac" "crypto/sha256" - "github.com/kataras/iris/core/errors" - "github.com/stretchr/testify/require" + "errors" "testing" + + require "github.com/stretchr/testify/require" ) func TestVersionOneOfMac(t *testing.T) { diff --git a/dapp/dapp.go b/dapp/dapp.go new file mode 100644 index 0000000..74025aa --- /dev/null +++ b/dapp/dapp.go @@ -0,0 +1,71 @@ +package dapp + +import ( + "encoding/hex" + "fmt" + + module "github.com/Bit-Nation/panthalassa/dapp/module" + logger "github.com/op/go-logging" + otto "github.com/robertkrimen/otto" +) + +type DApp struct { + vm *otto.Otto + logger *logger.Logger + app *JsonRepresentation + closeChan chan<- *JsonRepresentation +} + +// close DApp +func (d *DApp) Close() { + d.vm.Interrupt <- func() { + d.logger.Info(fmt.Sprintf("shutting down: %s (%s)", hex.EncodeToString(d.app.SignaturePublicKey), d.app.Name)) + d.closeChan <- d.app + } +} + +func (d *DApp) ID() string { + return hex.EncodeToString(d.app.SignaturePublicKey) +} + +// will start a DApp based on the given config file +// +func New(l *logger.Logger, app *JsonRepresentation, vmModules []module.Module, closer chan<- *JsonRepresentation) (*DApp, error) { + + // check if app is valid + valid, err := app.VerifySignature() + if err != nil { + return nil, err + } + if !valid { + return nil, InvalidSignature + } + + // create VM + vm := otto.New() + vm.Interrupt = make(chan func(), 1) + + // register all vm modules + for _, m := range vmModules { + if err := m.Register(vm); err != nil { + return nil, err + } + } + + dApp := &DApp{ + vm: vm, + logger: l, + app: app, + closeChan: closer, + } + + go func() { + _, err := vm.Run(app.Code) + if err != nil { + l.Error(err) + closer <- app + } + }() + + return dApp, nil +} diff --git a/dapp/dapp_representation.go b/dapp/dapp_representation.go new file mode 100644 index 0000000..fddf674 --- /dev/null +++ b/dapp/dapp_representation.go @@ -0,0 +1,59 @@ +package dapp + +import ( + "bytes" + "encoding/json" + "errors" + + mh "github.com/multiformats/go-multihash" + ed25519 "golang.org/x/crypto/ed25519" +) + +var InvalidSignature = errors.New("failed to verify signature for DApp") + +// JSON Representation of published DApp +type JsonRepresentation struct { + Name string `json:"name"` + Code string `json:"code"` + SignaturePublicKey []byte `json:"signature_public_key"` + Signature []byte `json:"signature"` +} + +// hash the published DApp +func (r JsonRepresentation) Hash() ([]byte, error) { + + buff := bytes.NewBuffer([]byte(r.Name)) + + if _, err := buff.Write([]byte(r.Code)); err != nil { + return nil, err + } + + if _, err := buff.Write(r.SignaturePublicKey); err != nil { + return nil, err + } + + multiHash, err := mh.Sum(buff.Bytes(), mh.SHA3_256, -1) + if err != nil { + return nil, err + } + + return multiHash, nil + +} + +// verify if this published DApp +// was signed with the attached public key +func (r JsonRepresentation) VerifySignature() (bool, error) { + + hash, err := r.Hash() + if err != nil { + return false, err + } + + return ed25519.Verify(r.SignaturePublicKey, hash, r.Signature), nil + +} + +func (r JsonRepresentation) Marshal() ([]byte, error) { + return json.Marshal(r) +} diff --git a/dapp/dapp_representation_test.go b/dapp/dapp_representation_test.go new file mode 100644 index 0000000..724d6e7 --- /dev/null +++ b/dapp/dapp_representation_test.go @@ -0,0 +1,75 @@ +package dapp + +import ( + "bytes" + "crypto/rand" + "golang.org/x/crypto/ed25519" + "testing" + + mh "github.com/multiformats/go-multihash" + require "github.com/stretchr/testify/require" +) + +func TestDAppRepresentationHash(t *testing.T) { + + pub, _, err := ed25519.GenerateKey(rand.Reader) + require.Nil(t, err) + + rep := JsonRepresentation{ + Name: "Send / Receive Money", + Code: `var wallet = "0x930aa9a843266bdb02847168d571e7913907dd84"`, + SignaturePublicKey: pub, + } + + // calculate hash manually + // name + code + signature public key + buff := bytes.NewBuffer([]byte(rep.Name)) + + _, err = buff.Write([]byte(rep.Code)) + require.Nil(t, err) + + _, err = buff.Write([]byte(rep.SignaturePublicKey)) + require.Nil(t, err) + + expectedHash, err := mh.Sum(buff.Bytes(), mh.SHA3_256, -1) + require.Nil(t, err) + + // calculate hash + calculateHash, err := rep.Hash() + require.Nil(t, err) + + // check if hashes match + require.Equal(t, string(expectedHash), string(calculateHash)) + +} + +func TestDAppVerifySignature(t *testing.T) { + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.Nil(t, err) + + rep := JsonRepresentation{ + Name: "Send / Receive Money", + Code: `var wallet = "0x930aa9a843266bdb02847168d571e7913907dd84"`, + SignaturePublicKey: pub, + } + + // validate signature + // should be invalid since it doesn't exist + valid, err := rep.VerifySignature() + require.Nil(t, err) + require.False(t, valid) + + // hash the representation + calculatedHash, err := rep.Hash() + require.Nil(t, err) + + // sign representation + rep.Signature = ed25519.Sign(priv, calculatedHash) + + // validate signature + valid, err = rep.VerifySignature() + require.Nil(t, err) + require.True(t, valid) + +} diff --git a/dapp/module/logger/module.go b/dapp/module/logger/module.go new file mode 100644 index 0000000..8db3bb6 --- /dev/null +++ b/dapp/module/logger/module.go @@ -0,0 +1,81 @@ +package logger + +import ( + "bufio" + "strings" + + pb "github.com/Bit-Nation/panthalassa/dapp/registry/pb" + net "github.com/libp2p/go-libp2p-net" + mc "github.com/multiformats/go-multicodec" + protoMc "github.com/multiformats/go-multicodec/protobuf" + logger "github.com/op/go-logging" + otto "github.com/robertkrimen/otto" +) + +type streamLogger struct { + writer *bufio.Writer + encoder mc.Encoder +} + +func (l *streamLogger) Write(data []byte) (int, error) { + + msg := pb.Message{ + Type: pb.Message_LOG, + Log: data, + } + + if err := l.encoder.Encode(&msg); err != nil { + return 0, err + } + + if err := l.writer.Flush(); err != nil { + return 0, err + } + + return len(data), nil +} + +type Logger struct { + Logger *logger.Logger +} + +func New(stream net.Stream) (*Logger, error) { + + l, err := logger.GetLogger("") + if err != nil { + return nil, err + } + + w := bufio.NewWriter(stream) + loggerStream := &streamLogger{ + writer: w, + encoder: protoMc.Multicodec(nil).Encoder(w), + } + + l.SetBackend(logger.AddModuleLevel(logger.NewLogBackend(loggerStream, "", 0))) + + return &Logger{ + Logger: l, + }, nil +} + +func (l *Logger) Name() string { + return "LOGGER" +} + +// Register a module that writes console.log +// to the given logger +func (l *Logger) Register(vm *otto.Otto) error { + + return vm.Set("console", map[string]interface{}{ + "log": func(call otto.FunctionCall) otto.Value { + toLog := []string{} + for _, arg := range call.ArgumentList { + toLog = append(toLog, arg.String()) + } + l.Logger.Info(strings.Join(toLog, ",")) + return otto.Value{} + }, + }) + +} diff --git a/dapp/module/logger/module_test.go b/dapp/module/logger/module_test.go new file mode 100644 index 0000000..fed6272 --- /dev/null +++ b/dapp/module/logger/module_test.go @@ -0,0 +1,86 @@ +package logger + +import ( + "testing" + + logger "github.com/op/go-logging" + otto "github.com/robertkrimen/otto" + require "github.com/stretchr/testify/require" +) + +type testValue struct { + js string + assertion func(consoleOut string) +} + +type testWriter struct { + assertion func(consoleOut string) +} + +func (w testWriter) Write(b []byte) (int, error) { + w.assertion(string(b)) + return 0, nil +} + +func TestLoggerModule(t *testing.T) { + + testValues := []testValue{ + testValue{ + js: `console.log(1, 2, 3)`, + assertion: func(consoleOut string) { + require.Equal(t, "1,2,3\n", consoleOut) + }, + }, + testValue{ + js: `console.log("hi","there")`, + assertion: func(consoleOut string) { + require.Equal(t, "hi,there\n", consoleOut) + }, + }, + testValue{ + js: `console.log({key: 4})`, + assertion: func(consoleOut string) { + require.Equal(t, "[object Object]\n", consoleOut) + }, + }, + testValue{ + js: ` + var cb = function(){}; + console.log(cb) + `, + assertion: func(consoleOut string) { + require.Equal(t, "function(){}\n", consoleOut) + }, + }, + testValue{ + js: `console.log("hi",1)`, + assertion: func(consoleOut string) { + require.Equal(t, "hi,1\n", consoleOut) + }, + }, + } + + for _, testValue := range testValues { + + // create VM + vm := otto.New() + + // create logger + b := logger.NewLogBackend(testWriter{ + assertion: testValue.assertion, + }, "", 0) + l, err := logger.GetLogger("-") + require.Nil(t, err) + l.SetBackend(logger.AddModuleLevel(b)) + + loggerModule, err := New(nil) + require.Nil(t, err) + loggerModule.Logger = l + loggerModule.Register(vm) + + _, err = vm.Run(testValue.js) + require.Nil(t, err) + + } + +} diff --git a/dapp/module/module.go b/dapp/module/module.go new file mode 100644 index 0000000..5a1c0d5 --- /dev/null +++ b/dapp/module/module.go @@ -0,0 +1,10 @@ +package module + +import ( + otto "github.com/robertkrimen/otto" +) + +type Module interface { + Name() string + Register(vm *otto.Otto) error +} diff --git a/dapp/module/uuidv4/module.go b/dapp/module/uuidv4/module.go new file mode 100644 index 0000000..ad27bf6 --- /dev/null +++ b/dapp/module/uuidv4/module.go @@ -0,0 +1,50 @@ +package uuidv4 + +import ( + otto "github.com/robertkrimen/otto" + uuid "github.com/satori/go.uuid" +) + +var newUuid = uuid.NewV4 + +// register an module on the given +// vm that allows to generate uuid's +// of version 4. it expects a +// callback as it's only argument +type UUIDV4 struct{} + +func (r *UUIDV4) Name() string { + return "UUIDV4" +} + +func (r *UUIDV4) Register(vm *otto.Otto) error { + + return vm.Set("uuidV4", func(call otto.FunctionCall) otto.Value { + + // make sure callback is a function + cb := call.Argument(0) + if !cb.IsFunction() { + _, err := cb.Call(call.This, nil, "expected first argument to be a function") + if err != nil { + // @todo process error + } + return otto.Value{} + } + + // create uuid + id, err := newUuid() + if err != nil { + _, err = cb.Call(call.This, nil, err.Error()) + return otto.Value{} + } + + // call callback with uuid + _, err = cb.Call(call.This, id.String(), nil) + if err != nil { + // @todo process error + } + return otto.Value{} + + }) + +} diff --git a/dapp/module/uuidv4/module_test.go b/dapp/module/uuidv4/module_test.go new file mode 100644 index 0000000..c9c0f06 --- /dev/null +++ b/dapp/module/uuidv4/module_test.go @@ -0,0 +1,54 @@ +package uuidv4 + +import ( + "errors" + "testing" + + otto "github.com/robertkrimen/otto" + uuid "github.com/satori/go.uuid" + require "github.com/stretchr/testify/require" +) + +func TestUUIDV4ModelSuccess(t *testing.T) { + + m := UUIDV4{} + + // mock the new uuid function + newUuid = func() (uuid.UUID, error) { + return uuid.FromString("9b781c39-2bd3-41c6-a246-150a9f4421a3") + } + + vm := otto.New() + vm.Set("test", func(call otto.FunctionCall) otto.Value { + require.Equal(t, "9b781c39-2bd3-41c6-a246-150a9f4421a3", call.Argument(0).String()) + require.True(t, call.Argument(1).IsUndefined()) + return otto.Value{} + }) + + require.Nil(t, m.Register(vm)) + + vm.Run(`uuidV4(test)`) + +} + +func TestUUIDV4ModelError(t *testing.T) { + + m := UUIDV4{} + // mock the new uuid function + newUuid = func() (uuid.UUID, error) { + return uuid.UUID{}, errors.New("I am a nice error message") + } + + vm := otto.New() + vm.Set("test", func(call otto.FunctionCall) otto.Value { + require.True(t, call.Argument(0).IsUndefined()) + require.Equal(t, "I am a nice error message", call.Argument(1).String()) + return otto.Value{} + }) + + require.Nil(t, m.Register(vm)) + + _, err := vm.Run(`uuidV4(test)`) + require.Nil(t, err) + +} diff --git a/dapp/registry/client.go b/dapp/registry/client.go new file mode 100644 index 0000000..d23ea1b --- /dev/null +++ b/dapp/registry/client.go @@ -0,0 +1,12 @@ +package registry + +import ( + "github.com/Bit-Nation/panthalassa/dapp" +) + +// the client is the "thing" that's using +// the DApps + Registry. It's responsible for +// saving the received DApp +type Client interface { + HandleReceivedDApp(dApp dapp.JsonRepresentation) error +} diff --git a/dapp/registry/pb/messages.pb.go b/dapp/registry/pb/messages.pb.go new file mode 100644 index 0000000..2244c2a --- /dev/null +++ b/dapp/registry/pb/messages.pb.go @@ -0,0 +1,119 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: dapp/registry/pb/messages.proto + +package api + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type Message_Type int32 + +const ( + Message_LOG Message_Type = 0 + Message_DApp Message_Type = 1 +) + +var Message_Type_name = map[int32]string{ + 0: "LOG", + 1: "DApp", +} +var Message_Type_value = map[string]int32{ + "LOG": 0, + "DApp": 1, +} + +func (x Message_Type) String() string { + return proto.EnumName(Message_Type_name, int32(x)) +} +func (Message_Type) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_messages_fea11ca9f69536ab, []int{0, 0} +} + +type Message struct { + Type Message_Type `protobuf:"varint,1,opt,name=type,enum=api.Message_Type" json:"type,omitempty"` + Log []byte `protobuf:"bytes,2,opt,name=log,proto3" json:"log,omitempty"` + DApp []byte `protobuf:"bytes,3,opt,name=dApp,proto3" json:"dApp,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Message) Reset() { *m = Message{} } +func (m *Message) String() string { return proto.CompactTextString(m) } +func (*Message) ProtoMessage() {} +func (*Message) Descriptor() ([]byte, []int) { + return fileDescriptor_messages_fea11ca9f69536ab, []int{0} +} +func (m *Message) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Message.Unmarshal(m, b) +} +func (m *Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Message.Marshal(b, m, deterministic) +} +func (dst *Message) XXX_Merge(src proto.Message) { + xxx_messageInfo_Message.Merge(dst, src) +} +func (m *Message) XXX_Size() int { + return xxx_messageInfo_Message.Size(m) +} +func (m *Message) XXX_DiscardUnknown() { + xxx_messageInfo_Message.DiscardUnknown(m) +} + +var xxx_messageInfo_Message proto.InternalMessageInfo + +func (m *Message) GetType() Message_Type { + if m != nil { + return m.Type + } + return Message_LOG +} + +func (m *Message) GetLog() []byte { + if m != nil { + return m.Log + } + return nil +} + +func (m *Message) GetDApp() []byte { + if m != nil { + return m.DApp + } + return nil +} + +func init() { + proto.RegisterType((*Message)(nil), "api.Message") + proto.RegisterEnum("api.Message_Type", Message_Type_name, Message_Type_value) +} + +func init() { + proto.RegisterFile("dapp/registry/pb/messages.proto", fileDescriptor_messages_fea11ca9f69536ab) +} + +var fileDescriptor_messages_fea11ca9f69536ab = []byte{ + // 159 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4f, 0x49, 0x2c, 0x28, + 0xd0, 0x2f, 0x4a, 0x4d, 0xcf, 0x2c, 0x2e, 0x29, 0xaa, 0xd4, 0x2f, 0x48, 0xd2, 0xcf, 0x4d, 0x2d, + 0x2e, 0x4e, 0x4c, 0x4f, 0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x4e, 0x2c, 0xc8, + 0x54, 0x2a, 0xe4, 0x62, 0xf7, 0x85, 0x08, 0x0b, 0xa9, 0x72, 0xb1, 0x94, 0x54, 0x16, 0xa4, 0x4a, + 0x30, 0x2a, 0x30, 0x6a, 0xf0, 0x19, 0x09, 0xea, 0x25, 0x16, 0x64, 0xea, 0x41, 0xe5, 0xf4, 0x42, + 0x2a, 0x0b, 0x52, 0x83, 0xc0, 0xd2, 0x42, 0x02, 0x5c, 0xcc, 0x39, 0xf9, 0xe9, 0x12, 0x4c, 0x0a, + 0x8c, 0x1a, 0x3c, 0x41, 0x20, 0xa6, 0x90, 0x10, 0x17, 0x4b, 0x8a, 0x63, 0x41, 0x81, 0x04, 0x33, + 0x58, 0x08, 0xcc, 0x56, 0x92, 0xe4, 0x62, 0x01, 0xe9, 0x11, 0x62, 0xe7, 0x62, 0xf6, 0xf1, 0x77, + 0x17, 0x60, 0x10, 0xe2, 0xe0, 0x62, 0x71, 0x71, 0x2c, 0x28, 0x10, 0x60, 0x4c, 0x62, 0x03, 0x5b, + 0x6f, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0xee, 0xbd, 0xd2, 0x39, 0xa1, 0x00, 0x00, 0x00, +} diff --git a/dapp/registry/pb/messages.proto b/dapp/registry/pb/messages.proto new file mode 100644 index 0000000..3345936 --- /dev/null +++ b/dapp/registry/pb/messages.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package api; + +message Message { + + enum Type { + LOG = 0; + DApp = 1; + } + + Type type = 1; + + bytes log = 2; + bytes dApp = 3; + +} \ No newline at end of file diff --git a/dapp/registry/registry.go b/dapp/registry/registry.go new file mode 100644 index 0000000..336da16 --- /dev/null +++ b/dapp/registry/registry.go @@ -0,0 +1,146 @@ +package registry + +import ( + "context" + "encoding/hex" + "errors" + "io/ioutil" + "sync" + + dapp "github.com/Bit-Nation/panthalassa/dapp" + module "github.com/Bit-Nation/panthalassa/dapp/module" + loggerMod "github.com/Bit-Nation/panthalassa/dapp/module/logger" + uuidv4Mod "github.com/Bit-Nation/panthalassa/dapp/module/uuidv4" + log "github.com/ipfs/go-log" + host "github.com/libp2p/go-libp2p-host" + net "github.com/libp2p/go-libp2p-net" + pstore "github.com/libp2p/go-libp2p-peerstore" + ma "github.com/multiformats/go-multiaddr" + golog "github.com/op/go-logging" +) + +var logger = log.Logger("dapp - registry") + +// keep track of all running DApps +type Registry struct { + host host.Host + lock sync.Mutex + dAppDevStreams map[string]net.Stream + dAppInstances map[string]*dapp.DApp + closeChan chan *dapp.JsonRepresentation + client Client +} + +// create new dApp registry +func NewDAppRegistry(h host.Host, client Client) *Registry { + + r := &Registry{ + host: h, + lock: sync.Mutex{}, + dAppDevStreams: map[string]net.Stream{}, + dAppInstances: map[string]*dapp.DApp{}, + closeChan: make(chan *dapp.JsonRepresentation), + client: client, + } + + // add worker to remove DApps + go func() { + for { + select { + case cc := <-r.closeChan: + r.lock.Lock() + delete(r.dAppInstances, hex.EncodeToString(cc.SignaturePublicKey)) + // @todo send signal to client that this app was shut down + r.lock.Unlock() + } + } + }() + + return r + +} + +// start a DApp +func (r *Registry) StartDApp(dApp *dapp.JsonRepresentation) error { + + vmModules := []module.Module{ + &uuidv4Mod.UUIDV4{}, + } + + var l *golog.Logger + l, err := golog.GetLogger("app name") + + // if there is a stream for this DApp + // we would like to mutate the logger + // to write to the stream we have for development + // this will write logs to the stream + exist, stream := r.getDAppDevStream(dApp.SignaturePublicKey) + if exist { + // append log module + logger, err := loggerMod.New(stream) + l = logger.Logger + if err != nil { + return err + } + vmModules = append(vmModules, logger) + } else { + if err != nil { + return err + } + l.SetBackend(golog.AddModuleLevel(golog.NewLogBackend(ioutil.Discard, "", 0))) + } + + app, err := dapp.New(l, dApp, vmModules, r.closeChan) + if err != nil { + return err + } + + // add DApp to state + r.lock.Lock() + r.dAppInstances[app.ID()] = app + r.lock.Unlock() + + return nil + +} + +// use this to connect to a development server +func (r *Registry) ConnectDevelopmentServer(addr ma.Multiaddr) error { + + // address to peer info + pInfo, err := pstore.InfoFromP2pAddr(addr) + if err != nil { + return err + } + + // connect to peer + if err := r.host.Connect(context.Background(), *pInfo); err != nil { + return err + } + + // create stream to development peer + str, err := r.host.NewStream(context.Background(), pInfo.ID, "/dapp-development/0.0.0") + if err != nil { + return err + } + + // handle stream + r.devStreamHandler(str) + + return nil +} + +func (r *Registry) ShutDown(dAppJson dapp.JsonRepresentation) error { + + // shut down DApp & remove from state + r.lock.Lock() + dApp, exist := r.dAppInstances[hex.EncodeToString(dAppJson.SignaturePublicKey)] + if !exist { + return errors.New("DApp is not running") + } + dApp.Close() + r.lock.Unlock() + + return nil + +} diff --git a/dapp/registry/streams.go b/dapp/registry/streams.go new file mode 100644 index 0000000..dee6af5 --- /dev/null +++ b/dapp/registry/streams.go @@ -0,0 +1,66 @@ +package registry + +import ( + "bufio" + "encoding/json" + + dapp "github.com/Bit-Nation/panthalassa/dapp" + pb "github.com/Bit-Nation/panthalassa/dapp/registry/pb" + net "github.com/libp2p/go-libp2p-net" + protoMc "github.com/multiformats/go-multicodec/protobuf" +) + +// this stream handler is used for development purpose +// when we receive a DApp we will send it to the client +// the client will then decide what to do with it. +func (r *Registry) devStreamHandler(str net.Stream) { + + go func() { + + reader := bufio.NewReader(str) + decoder := protoMc.Multicodec(nil).Decoder(reader) + + for { + + // decode app from stream + msg := pb.Message{} + if err := decoder.Decode(&msg); err != nil { + logger.Error(err) + continue + } + + if msg.Type != pb.Message_DApp { + logger.Error("i can only handle DApps") + continue + } + + var app dapp.JsonRepresentation + if err := json.Unmarshal(msg.DApp, &app); err != nil { + logger.Error(err) + continue + } + + // add stream to registry so that we can + // associate it with the DApp + r.addDAppDevStream(app.SignaturePublicKey, str) + + valid, err := app.VerifySignature() + if err != nil { + logger.Error(err) + continue + } + if !valid { + logger.Error("Received invalid signature for DApp: ", app.Name) + continue + } + + // push received DApp upstream + if err := r.client.HandleReceivedDApp(app); err != nil { + logger.Error(err) + } + + } + + }() + +} diff --git a/dapp/registry/streams_test.go b/dapp/registry/streams_test.go new file mode 100644 index 0000000..5ad86b2 --- /dev/null +++ b/dapp/registry/streams_test.go @@ -0,0 +1,100 @@ +package registry + +import ( + "errors" + "time" + + crypto "github.com/libp2p/go-libp2p-crypto" + net "github.com/libp2p/go-libp2p-net" + peer "github.com/libp2p/go-libp2p-peer" + ma "github.com/multiformats/go-multiaddr" +) + +// test stream implementation +type stream struct { + net.Stream + data []byte + failRead, failWrite, failClose bool + reset bool + conn net.Conn +} + +func (s *stream) Reset() error { + s.reset = true + return nil +} + +func (s *stream) Close() error { + return errors.New("Close is not implemented") +} + +func (s *stream) SetDeadline(t time.Time) error { + return errors.New("SetDeadline is not implemented") +} + +func (s *stream) SetReadDeadline(t time.Time) error { + return errors.New("SetReadDeadline is not implemented") +} + +func (s *stream) SetWriteDeadline(t time.Time) error { + return errors.New("SetWriteDeadline is not implemented") +} + +func (s *stream) Write(b []byte) (int, error) { + return 0, errors.New("write is not implemented") +} + +func (s *stream) Read(b []byte) (int, error) { + return 0, errors.New("read is not implemented") +} + +func (s *stream) Conn() net.Conn { + return s.conn +} + +type conn struct { + remotePeerId peer.ID +} + +func (c *conn) Close() error { + return errors.New("Close not implemented") +} + +func (c *conn) NewStream() (net.Stream, error) { + return nil, errors.New("NewStream not implemented") +} + +func (c *conn) GetStreams() []net.Stream { + panic("GetStreams not implemented") +} + +// LocalMultiaddr is the Multiaddr on this side +func (c *conn) LocalMultiaddr() ma.Multiaddr { + panic("LocalMultiaddr - not implemented") +} + +// LocalPeer is the Peer on our side of the connection +func (c *conn) LocalPeer() peer.ID { + panic("LocalPeer - not implemented") +} + +// LocalPrivateKey is the private key of the peer on our side. +func (c *conn) LocalPrivateKey() crypto.PrivKey { + panic("LocalPrivateKey - not implemented") +} + +// RemoteMultiaddr is the Multiaddr on the remote side +func (c *conn) RemoteMultiaddr() ma.Multiaddr { + panic("RemoteMultiaddr - not implemented") +} + +// RemotePeer is the Peer on the remote side +func (c *conn) RemotePeer() peer.ID { + return c.remotePeerId +} + +// RemotePublicKey is the private key of the peer on our side. +func (c *conn) RemotePublicKey() crypto.PubKey { + panic("not implemented") + return nil +} diff --git a/dapp/registry/utils.go b/dapp/registry/utils.go new file mode 100644 index 0000000..b1acc03 --- /dev/null +++ b/dapp/registry/utils.go @@ -0,0 +1,25 @@ +package registry + +import ( + "encoding/hex" + + net "github.com/libp2p/go-libp2p-net" +) + +// add a stream that relates to DApp development +// to the registry. Use this to add a stream +// in a thread safe way +func (r *Registry) addDAppDevStream(key []byte, str net.Stream) { + r.lock.Lock() + defer r.lock.Unlock() + r.dAppDevStreams[hex.EncodeToString(key)] = str +} + +// get a stream that relates to DApp development +// from the registry. Use this for thread safety +func (r *Registry) getDAppDevStream(key []byte) (bool, net.Stream) { + r.lock.Lock() + defer r.lock.Unlock() + exist, stream := r.dAppDevStreams[hex.EncodeToString(key)] + return stream, exist +} diff --git a/dapp/registry/utils_test.go b/dapp/registry/utils_test.go new file mode 100644 index 0000000..b6ca580 --- /dev/null +++ b/dapp/registry/utils_test.go @@ -0,0 +1,36 @@ +package registry + +import ( + "sync" + "testing" + + net "github.com/libp2p/go-libp2p-net" + "github.com/stretchr/testify/require" +) + +// test add / get DApp dev stream +func TestAddGetDAppDevStream(t *testing.T) { + + reg := Registry{ + lock: sync.Mutex{}, + dAppDevStreams: map[string]net.Stream{}, + } + + s := &stream{} + + key := []byte("my_app_id") + + // test what happens if stream doesn't exist + exist, str := reg.getDAppDevStream(key) + require.Nil(t, str) + require.False(t, exist) + + // add stream + reg.addDAppDevStream(key, s) + + // get stream + exist, str = reg.getDAppDevStream(key) + require.Equal(t, s, str) + require.True(t, exist) + +} diff --git a/dapp/validator/call.go b/dapp/validator/call.go new file mode 100644 index 0000000..6c14c7b --- /dev/null +++ b/dapp/validator/call.go @@ -0,0 +1,77 @@ +package validator + +import ( + "errors" + "fmt" + "sync" + + otto "github.com/robertkrimen/otto" +) + +const ( + TypeFunction = iota + TypeNumber = iota + TypeObject = iota +) + +var validators = map[int]func(call otto.FunctionCall, position int) error{ + TypeFunction: func(call otto.FunctionCall, position int) error { + if !call.Argument(position).IsFunction() { + return errors.New(fmt.Sprintf("expected parameter %d to be of type function", position)) + } + return nil + }, + TypeNumber: func(call otto.FunctionCall, position int) error { + if !call.Argument(position).IsNumber() { + return errors.New(fmt.Sprintf("expected parameter %d to be of type number", position)) + } + return nil + }, + TypeObject: func(call otto.FunctionCall, position int) error { + if !call.Argument(position).IsObject() { + return errors.New(fmt.Sprintf("expected parameter %d to be of type object", position)) + } + return nil + }, +} + +type CallValidator struct { + lock sync.Mutex + rules map[int]int +} + +// add validation rule +func (v *CallValidator) Set(index int, expectedType int) error { + v.lock.Lock() + defer v.lock.Unlock() + _, exist := validators[expectedType] + if !exist { + return errors.New("type does not exist") + } + v.rules[index] = expectedType + return nil +} + +func (v *CallValidator) Validate(call otto.FunctionCall) error { + + v.lock.Lock() + defer v.lock.Unlock() + for index, expectedType := range v.rules { + validator, exist := validators[expectedType] + if !exist { + return errors.New(fmt.Sprintf("couldn't find validator for type: %d", index)) + } + if err := validator(call, index); err != nil { + return err + } + } + return nil + +} + +func New() *CallValidator { + return &CallValidator{ + lock: sync.Mutex{}, + rules: map[int]int{}, + } +} diff --git a/dapp/validator/doc.go b/dapp/validator/doc.go new file mode 100644 index 0000000..81f8074 --- /dev/null +++ b/dapp/validator/doc.go @@ -0,0 +1,3 @@ +package validator + +// provide a way to validate otto function calls diff --git a/package.json b/package.json index fbeff41..e7c6c66 100644 --- a/package.json +++ b/package.json @@ -37,30 +37,12 @@ "name": "go-cid", "version": "0.7.20" }, - { - "author": "jbenet", - "hash": "QmeiCcJfDW1GJnWUArudsv5rQsihpi4oyddPhdqo3CfX6i", - "name": "go-datastore", - "version": "2.4.1" - }, - { - "author": "jbenet", - "hash": "QmPpegoMqhAEqjncrzArm7KVWAkCm78rqL2DPuNjhPrshg", - "name": "go-datastore", - "version": "1.4.1" - }, { "author": "florian", "hash": "QmQVFBtJk8WXP7dVuAdcom5o7vRZF9pNBN5ZghuLvz5rLy", "name": "go-libp2p-bootstrap", "version": "1.0.0" }, - { - "author": "whyrusleeping", - "hash": "Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5", - "name": "go-libp2p-crypto", - "version": "1.6.2" - }, { "hash": "QmSBxn1eLMdViZRDGW9rRHRYwtqq5bqUgipqTMPuTim616", "name": "go-libp2p-kad-dht", @@ -76,6 +58,30 @@ "hash": "QmcJukH2sAFjY3HdBKq35WDzWoL3UUu2gt9wdfqZTUyM74", "name": "go-libp2p-peer", "version": "2.3.2" + }, + { + "author": "whyrusleeping", + "hash": "QmXauCuJzmzapetmC6W4TuDJLL1yFFrVzSHoWv8YdbmnxH", + "name": "go-libp2p-peerstore", + "version": "1.4.15" + }, + { + "author": "jbenet", + "hash": "QmeiCcJfDW1GJnWUArudsv5rQsihpi4oyddPhdqo3CfX6i", + "name": "go-datastore", + "version": "2.4.1" + }, + { + "author": "whyrusleeping", + "hash": "QmfZTdmunzKzAGJrSvXXQbQ5kLLUiEMX5vdwux7iXkdk7D", + "name": "go-libp2p-host", + "version": "2.1.7" + }, + { + "author": "whyrusleeping", + "hash": "Qme1knMqwt1hKZbc1BmQFmnm9f36nyQGwXxPGVpVJ9rMK5", + "name": "go-libp2p-crypto", + "version": "1.6.2" } ], "gxVersion": "0.12.1", diff --git a/state/state.go b/state/state.go new file mode 100644 index 0000000..955def9 --- /dev/null +++ b/state/state.go @@ -0,0 +1,38 @@ +package state + +import ( + "github.com/libp2p/go-libp2p-peer" + "sync" +) + +type State struct { + lock sync.Mutex + dappDevPeers []string +} + +func New() *State { + return &State{ + lock: sync.Mutex{}, + dappDevPeers: []string{}, + } +} + +// add a peer that is whitelisted +// for DApp Development +func (s *State) AddDAppDevPeer(pi peer.ID) { + s.lock.Lock() + defer s.lock.Unlock() + s.dappDevPeers = append(s.dappDevPeers, pi.Pretty()) +} + +// remove a peer from the DApp Development list +func (s *State) HasDAppDevPeer(pi peer.ID) bool { + s.lock.Lock() + defer s.lock.Unlock() + for _, peerId := range s.dappDevPeers { + if peerId == pi.Pretty() { + return true + } + } + return false +} diff --git a/state/state_test.go b/state/state_test.go new file mode 100644 index 0000000..484ef17 --- /dev/null +++ b/state/state_test.go @@ -0,0 +1,28 @@ +package state + +import ( + "testing" + + require "github.com/stretchr/testify/require" +) + +func TestState_AddDAppDevPeer(t *testing.T) { + + s := New() + s.AddDAppDevPeer("fake-peer-id") + + // The value is "2w4H9nYaM9EYbazsd" since + // peer id is a struct that implements "String()" + // with a encoding function + require.Equal(t, "2w4H9nYaM9EYbazsd", s.dappDevPeers[0]) + +} + +func TestState_HasDAppDevPeer(t *testing.T) { + + s := New() + s.AddDAppDevPeer("fake-peer-id") + + require.True(t, s.HasDAppDevPeer("fake-peer-id")) + +}