diff --git a/catalog/services.go b/catalog/services.go index cd28b663e04c925eadd329a49f82aa84aec824de..24ab74a466f6499654267861760b72cbc2d6472e 100644 --- a/catalog/services.go +++ b/catalog/services.go @@ -23,4 +23,6 @@ const ( Group = "group" EndFT = "endFT" GroupRq = "groupRq" + + RestLike = "restLike" ) diff --git a/restlike/generateProto.sh b/restlike/generateProto.sh new file mode 100755 index 0000000000000000000000000000000000000000..67b6d293f6f4e6a68eff4162a42acb242129cd18 --- /dev/null +++ b/restlike/generateProto.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +protoc --go_out=paths=source_relative:. restlike/restLikeMessages.proto diff --git a/restlike/receiver.go b/restlike/receiver.go new file mode 100644 index 0000000000000000000000000000000000000000..7a137631bea709c8b9d7dabbb0888fec60a60cdc --- /dev/null +++ b/restlike/receiver.go @@ -0,0 +1,62 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/single" + "google.golang.org/protobuf/proto" + "time" +) + +// processor is the reception handler for a RestServer +type singleReceiver struct { + endpoints *Endpoints +} + +// Callback is the handler for single-use message reception for a RestServer +// Automatically responds to invalid endpoint requests +func (s *singleReceiver) Callback(req *single.Request, receptionId receptionID.EphemeralIdentity, rounds []rounds.Round) { + // Unmarshal the request payload + newMessage := &Message{} + err := proto.Unmarshal(req.GetPayload(), newMessage) + if err != nil { + jww.ERROR.Printf("Unable to unmarshal restlike message: %+v", err) + return + } + + var respondErr error + if cb, err := s.endpoints.Get(URI(newMessage.GetUri()), Method(newMessage.GetMethod())); err == nil { + // Send the payload to the proper Callback if it exists and respond with the result + respondErr = respond(cb(newMessage), req) + } else { + // If no callback, automatically send an error response + respondErr = respond(&Message{Error: err.Error()}, req) + } + if respondErr != nil { + jww.ERROR.Printf("Unable to respond to request: %+v", err) + } +} + +// respond to a single.Request with the given Message +func respond(response *Message, req *single.Request) error { + payload, err := proto.Marshal(response) + if err != nil { + return errors.Errorf("unable to marshal restlike response message: %+v", err) + } + + // TODO: Parameterize params and timeout + _, err = req.Respond(payload, cmix.GetDefaultCMIXParams(), 30*time.Second) + if err != nil { + return errors.Errorf("unable to send restlike response message: %+v", err) + } + return nil +} diff --git a/restlike/receiver_test.go b/restlike/receiver_test.go new file mode 100644 index 0000000000000000000000000000000000000000..56a46586daaf6411d589b79c0e179ade2ce4e017 --- /dev/null +++ b/restlike/receiver_test.go @@ -0,0 +1,60 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/single" + "testing" +) + +// Test failure of proto unmarshal +func TestSingleReceiver_Callback_FailUnmarshal(t *testing.T) { + ep := &Endpoints{endpoints: make(map[URI]map[Method]Callback)} + receiver := singleReceiver{endpoints: ep} + + testReq := single.BuildTestRequest(make([]byte, 0), t) + receiver.Callback(testReq, receptionID.EphemeralIdentity{}, nil) +} + +// Test happy path +//func TestSingleReceiver_Callback(t *testing.T) { +// ep := &Endpoints{endpoints: make(map[URI]map[Method]Callback)} +// resultChan := make(chan interface{}, 1) +// cb := func(*Message) *Message { +// resultChan <- "" +// return nil +// } +// testPath := URI("test/path") +// testMethod := Get +// testMessage := &Message{ +// Content: []byte("test"), +// Headers: nil, +// Method: uint32(testMethod), +// Uri: string(testPath), +// Error: "", +// } +// +// err := ep.Add(testPath, testMethod, cb) +// if err != nil { +// t.Errorf(err.Error()) +// } +// receiver := singleReceiver{endpoints: ep} +// +// testPayload, err := proto.Marshal(testMessage) +// if err != nil { +// t.Errorf(err.Error()) +// } +// testReq := single.BuildTestRequest(testPayload, t) +// receiver.Callback(testReq, receptionID.EphemeralIdentity{}, nil) +// +// select { +// case _ = <-resultChan: +// case <-time.After(3 * time.Second): +// t.Errorf("Test SingleReceiver timed out!") +// } +//} diff --git a/restlike/request.go b/restlike/request.go new file mode 100644 index 0000000000000000000000000000000000000000..5b9e0427089dbeb53a4474dbb0f2afa01112e137 --- /dev/null +++ b/restlike/request.go @@ -0,0 +1,84 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/catalog" + "gitlab.com/elixxir/client/single" + "gitlab.com/elixxir/crypto/contact" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/xx_network/crypto/csprng" + "google.golang.org/protobuf/proto" +) + +// SingleRequest allows for making REST-like requests to a RestServer using single-use messages +// Can be used as stateful or declared inline without state +type SingleRequest struct { + Net single.Cmix + Rng csprng.Source + E2eGrp *cyclic.Group +} + +// Request provides several Method of sending Data to the given URI +// and blocks until the Message is returned +func (s *SingleRequest) Request(method Method, recipient contact.Contact, path URI, + content Data, headers *Headers, singleParams single.RequestParams) (*Message, error) { + // Build the Message + newMessage := &Message{ + Content: content, + Headers: headers, + Method: uint32(method), + Uri: string(path), + } + msg, err := proto.Marshal(newMessage) + if err != nil { + return nil, err + } + + // Build callback for the single-use response + signalChannel := make(chan *Message, 1) + cb := func(msg *Message) { + signalChannel <- msg + } + + // Transmit the Message + _, _, err = single.TransmitRequest(recipient, catalog.RestLike, msg, + &singleResponse{responseCallback: cb}, singleParams, s.Net, s.Rng, s.E2eGrp) + if err != nil { + return nil, err + } + + // Block waiting for single-use response + jww.DEBUG.Printf("Restlike waiting for single-use response from %s...", recipient.ID.String()) + newResponse := <-signalChannel + jww.DEBUG.Printf("Restlike single-use response received from %s", recipient.ID.String()) + + return newResponse, nil +} + +// AsyncRequest provides several Method of sending Data to the given URI +// and will return the Message to the given Callback when received +func (s *SingleRequest) AsyncRequest(method Method, recipient contact.Contact, path URI, + content Data, headers *Headers, cb RequestCallback, singleParams single.RequestParams) error { + // Build the Message + newMessage := &Message{ + Content: content, + Headers: headers, + Method: uint32(method), + Uri: string(path), + } + msg, err := proto.Marshal(newMessage) + if err != nil { + return err + } + + // Transmit the Message + _, _, err = single.TransmitRequest(recipient, catalog.RestLike, msg, + &singleResponse{responseCallback: cb}, singleParams, s.Net, s.Rng, s.E2eGrp) + return err +} diff --git a/restlike/response.go b/restlike/response.go new file mode 100644 index 0000000000000000000000000000000000000000..41e21b6061c110eaaa06fc25a472bf0326ca1ff3 --- /dev/null +++ b/restlike/response.go @@ -0,0 +1,39 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + "google.golang.org/protobuf/proto" +) + +// processor is the response handler for a Request +type singleResponse struct { + responseCallback RequestCallback +} + +// Callback is the handler for single-use message responses for a Request +func (s *singleResponse) Callback(payload []byte, receptionID receptionID.EphemeralIdentity, rounds []rounds.Round, err error) { + newMessage := &Message{} + + // Handle response errors + if err != nil { + newMessage.Error = err.Error() + s.responseCallback(newMessage) + return + } + + // Unmarshal the payload + err = proto.Unmarshal(payload, newMessage) + if err != nil { + newMessage.Error = err.Error() + } + + // Send the response payload to the responseCallback + s.responseCallback(newMessage) +} diff --git a/restlike/response_test.go b/restlike/response_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0381b452cca26c9d892abfe2b8329569a1eef689 --- /dev/null +++ b/restlike/response_test.go @@ -0,0 +1,96 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + "bytes" + "github.com/pkg/errors" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "google.golang.org/protobuf/proto" + "testing" + "time" +) + +// Test happy path +func TestSingleResponse_Callback(t *testing.T) { + resultChan := make(chan *Message, 1) + cb := func(input *Message) { + resultChan <- input + } + testPath := "test/path" + testMethod := Get + testMessage := &Message{ + Content: []byte("test"), + Headers: nil, + Method: uint32(testMethod), + Uri: testPath, + Error: "", + } + + response := singleResponse{cb} + + testPayload, err := proto.Marshal(testMessage) + if err != nil { + t.Errorf(err.Error()) + } + response.Callback(testPayload, receptionID.EphemeralIdentity{}, nil, nil) + + select { + case result := <-resultChan: + if result.Uri != testPath { + t.Errorf("Mismatched uri") + } + if result.Method != uint32(testMethod) { + t.Errorf("Mismatched method") + } + if !bytes.Equal(testMessage.Content, result.Content) { + t.Errorf("Mismatched content") + } + case <-time.After(3 * time.Second): + t.Errorf("Test SingleResponse timed out!") + } +} + +// Test error input path +func TestSingleResponse_Callback_Err(t *testing.T) { + resultChan := make(chan *Message, 1) + cb := func(input *Message) { + resultChan <- input + } + response := singleResponse{cb} + + response.Callback(nil, receptionID.EphemeralIdentity{}, nil, errors.New("test")) + + select { + case result := <-resultChan: + if len(result.Error) == 0 { + t.Errorf("Expected cb error!") + } + case <-time.After(3 * time.Second): + t.Errorf("Test SingleResponse input error timed out!") + } +} + +// Test proto error path +func TestSingleResponse_Callback_ProtoErr(t *testing.T) { + resultChan := make(chan *Message, 1) + cb := func(input *Message) { + resultChan <- input + } + response := singleResponse{cb} + + response.Callback([]byte("test"), receptionID.EphemeralIdentity{}, nil, nil) + + select { + case result := <-resultChan: + if len(result.Error) == 0 { + t.Errorf("Expected cb proto error!") + } + case <-time.After(3 * time.Second): + t.Errorf("Test SingleResponse proto error timed out!") + } +} diff --git a/restlike/restLikeMessages.pb.go b/restlike/restLikeMessages.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..c6666a5d08dd3c0a086a100a19ca0a74f630af79 --- /dev/null +++ b/restlike/restLikeMessages.pb.go @@ -0,0 +1,270 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.19.1 +// source: restlike/restLikeMessages.proto + +package restlike + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Message are used for sending to and receiving from a RestServer +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Content []byte `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + Headers *Headers `protobuf:"bytes,2,opt,name=headers,proto3" json:"headers,omitempty"` + Method uint32 `protobuf:"varint,3,opt,name=method,proto3" json:"method,omitempty"` + Uri string `protobuf:"bytes,4,opt,name=uri,proto3" json:"uri,omitempty"` + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_restlike_restLikeMessages_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_restlike_restLikeMessages_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_restlike_restLikeMessages_proto_rawDescGZIP(), []int{0} +} + +func (x *Message) GetContent() []byte { + if x != nil { + return x.Content + } + return nil +} + +func (x *Message) GetHeaders() *Headers { + if x != nil { + return x.Headers + } + return nil +} + +func (x *Message) GetMethod() uint32 { + if x != nil { + return x.Method + } + return 0 +} + +func (x *Message) GetUri() string { + if x != nil { + return x.Uri + } + return "" +} + +func (x *Message) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// Headers allows different configurations for each Request +// that will be specified in the Request header +type Headers struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Headers allows for custom headers to be included with a Request + Headers []byte `protobuf:"bytes,1,opt,name=headers,proto3" json:"headers,omitempty"` + // Version allows for endpoints to be backwards-compatible + // and handle different formats of the same Request + Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` +} + +func (x *Headers) Reset() { + *x = Headers{} + if protoimpl.UnsafeEnabled { + mi := &file_restlike_restLikeMessages_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Headers) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Headers) ProtoMessage() {} + +func (x *Headers) ProtoReflect() protoreflect.Message { + mi := &file_restlike_restLikeMessages_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Headers.ProtoReflect.Descriptor instead. +func (*Headers) Descriptor() ([]byte, []int) { + return file_restlike_restLikeMessages_proto_rawDescGZIP(), []int{1} +} + +func (x *Headers) GetHeaders() []byte { + if x != nil { + return x.Headers + } + return nil +} + +func (x *Headers) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +var File_restlike_restLikeMessages_proto protoreflect.FileDescriptor + +var file_restlike_restLikeMessages_proto_rawDesc = []byte{ + 0x0a, 0x1f, 0x72, 0x65, 0x73, 0x74, 0x6c, 0x69, 0x6b, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x4c, + 0x69, 0x6b, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x12, 0x10, 0x72, 0x65, 0x73, 0x74, 0x4c, 0x69, 0x6b, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x73, 0x22, 0x98, 0x01, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x33, 0x0a, 0x07, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, + 0x74, 0x4c, 0x69, 0x6b, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x48, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x73, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x16, + 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x69, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x69, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x3d, + 0x0a, 0x07, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x68, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x24, 0x5a, + 0x22, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x6c, 0x69, 0x78, + 0x78, 0x69, 0x72, 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x6c, + 0x69, 0x6b, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_restlike_restLikeMessages_proto_rawDescOnce sync.Once + file_restlike_restLikeMessages_proto_rawDescData = file_restlike_restLikeMessages_proto_rawDesc +) + +func file_restlike_restLikeMessages_proto_rawDescGZIP() []byte { + file_restlike_restLikeMessages_proto_rawDescOnce.Do(func() { + file_restlike_restLikeMessages_proto_rawDescData = protoimpl.X.CompressGZIP(file_restlike_restLikeMessages_proto_rawDescData) + }) + return file_restlike_restLikeMessages_proto_rawDescData +} + +var file_restlike_restLikeMessages_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_restlike_restLikeMessages_proto_goTypes = []interface{}{ + (*Message)(nil), // 0: restLikeMessages.Message + (*Headers)(nil), // 1: restLikeMessages.Headers +} +var file_restlike_restLikeMessages_proto_depIdxs = []int32{ + 1, // 0: restLikeMessages.Message.headers:type_name -> restLikeMessages.Headers + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_restlike_restLikeMessages_proto_init() } +func file_restlike_restLikeMessages_proto_init() { + if File_restlike_restLikeMessages_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_restlike_restLikeMessages_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_restlike_restLikeMessages_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Headers); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_restlike_restLikeMessages_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_restlike_restLikeMessages_proto_goTypes, + DependencyIndexes: file_restlike_restLikeMessages_proto_depIdxs, + MessageInfos: file_restlike_restLikeMessages_proto_msgTypes, + }.Build() + File_restlike_restLikeMessages_proto = out.File + file_restlike_restLikeMessages_proto_rawDesc = nil + file_restlike_restLikeMessages_proto_goTypes = nil + file_restlike_restLikeMessages_proto_depIdxs = nil +} diff --git a/restlike/restLikeMessages.proto b/restlike/restLikeMessages.proto new file mode 100644 index 0000000000000000000000000000000000000000..00156d5443904383deb89f64c391e0009741dc78 --- /dev/null +++ b/restlike/restLikeMessages.proto @@ -0,0 +1,30 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +syntax = "proto3"; +package restLikeMessages; +option go_package = "gitlab.com/elixxir/client/restlike"; + +// Message are used for sending to and receiving from a RestServer +message Message { + bytes content = 1; + Headers headers = 2; + uint32 method = 3; + string uri = 4; + string error = 5; +} + +// Headers allows different configurations for each Request +// that will be specified in the Request header +message Headers { + // Headers allows for custom headers to be included with a Request + bytes headers = 1; + + // Version allows for endpoints to be backwards-compatible + // and handle different formats of the same Request + uint32 version = 2; +} \ No newline at end of file diff --git a/restlike/restServer.go b/restlike/restServer.go new file mode 100644 index 0000000000000000000000000000000000000000..dccb13ad3da83850bdd98be9b568dfab5c933102 --- /dev/null +++ b/restlike/restServer.go @@ -0,0 +1,67 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + "gitlab.com/elixxir/client/catalog" + "gitlab.com/elixxir/client/single" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/xx_network/primitives/id" +) + +// RestServer allows for clients to make REST-like requests this client +type RestServer interface { + // RegisterEndpoint allows the association of a Callback with + // a specific URI and a variety of different REST Method + RegisterEndpoint(path URI, method Method, cb Callback) error + + // UnregisterEndpoint removes the Callback associated with + // a specific URI and REST Method + UnregisterEndpoint(path URI, method Method) error + + // Close the internal RestServer endpoints and external services + Close() +} + +// singleServer implements the RestServer interface using single-use +type singleServer struct { + receptionId *id.ID + listener single.Listener + endpoints *Endpoints +} + +// NewSingleServer builds a RestServer with single-use and +// the provided arguments, then registers necessary external services +func NewSingleServer(receptionId *id.ID, privKey *cyclic.Int, net single.ListenCmix, e2eGrp *cyclic.Group) RestServer { + newServer := &singleServer{ + receptionId: receptionId, + endpoints: &Endpoints{endpoints: make(map[URI]map[Method]Callback)}, + } + newServer.listener = single.Listen(catalog.RestLike, receptionId, privKey, + net, e2eGrp, &singleReceiver{newServer.endpoints}) + return newServer +} + +// RegisterEndpoint allows the association of a Callback with +// a specific URI and a variety of different REST Method +func (r *singleServer) RegisterEndpoint(path URI, method Method, cb Callback) error { + return r.endpoints.Add(path, method, cb) +} + +// UnregisterEndpoint removes the Callback associated with +// a specific URI and REST Method +func (r *singleServer) UnregisterEndpoint(path URI, method Method) error { + return r.endpoints.Remove(path, method) +} + +// Close the internal RestServer endpoints and external services +func (r *singleServer) Close() { + // Clear all internal endpoints + r.endpoints = nil + // Destroy external services + r.listener.Stop() +} diff --git a/restlike/types.go b/restlike/types.go new file mode 100644 index 0000000000000000000000000000000000000000..247943844613a43c99b5d666f412a5c53f078471 --- /dev/null +++ b/restlike/types.go @@ -0,0 +1,116 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + "github.com/pkg/errors" + "sync" +) + +// URI defines the destination endpoint of a Request +type URI string + +// Data provides a generic structure for data sent with a Request or received in a Message +// NOTE: The way this is encoded is up to the implementation. For example, protobuf or JSON +type Data []byte + +// Method defines the possible Request types +type Method uint32 + +// RequestCallback provides the ability to make asynchronous Request +// in order to get the Message later without blocking +type RequestCallback func(*Message) + +// Callback serves as an Endpoint function to be called when a Request is received +// Should return the desired response to be sent back to the sender +type Callback func(*Message) *Message + +const ( + // Undefined default value + Undefined Method = iota + // Get retrieve an existing resource. + Get + // Post creates a new resource. + Post + // Put updates an existing resource. + Put + // Patch partially updates an existing resource. + Patch + // Delete a resource. + Delete +) + +// methodStrings is a map of Method values back to their constant names for printing +var methodStrings = map[Method]string{ + Undefined: "undefined", + Get: "get", + Post: "post", + Put: "put", + Patch: "patch", + Delete: "delete", +} + +// String returns the Method as a human-readable name. +func (m Method) String() string { + if methodStr, ok := methodStrings[m]; ok { + return methodStr + } + return methodStrings[Undefined] +} + +// Endpoints represents a map of internal endpoints for a RestServer +type Endpoints struct { + endpoints map[URI]map[Method]Callback + sync.RWMutex +} + +// Add a new Endpoint +// Returns an error if Endpoint already exists +func (e *Endpoints) Add(path URI, method Method, cb Callback) error { + e.Lock() + defer e.Unlock() + + if _, ok := e.endpoints[path]; !ok { + e.endpoints[path] = make(map[Method]Callback) + } + if _, ok := e.endpoints[path][method]; ok { + return errors.Errorf("unable to RegisterEndpoint: %s/%s already exists", path, method) + } + e.endpoints[path][method] = cb + return nil +} + +// Get an Endpoint +// Returns an error if Endpoint does not exist +func (e *Endpoints) Get(path URI, method Method) (Callback, error) { + e.RLock() + defer e.RUnlock() + + if _, ok := e.endpoints[path]; !ok { + return nil, errors.Errorf("unable to locate endpoint: %s", path) + } + if _, innerOk := e.endpoints[path][method]; !innerOk { + return nil, errors.Errorf("unable to locate endpoint: %s/%s", path, method) + } + return e.endpoints[path][method], nil +} + +// Remove an Endpoint +// Returns an error if Endpoint does not exist +func (e *Endpoints) Remove(path URI, method Method) error { + if _, err := e.Get(path, method); err != nil { + return errors.Errorf("unable to UnregisterEndpoint: %s", err.Error()) + } + + e.Lock() + defer e.Unlock() + delete(e.endpoints[path], method) + if len(e.endpoints[path]) == 0 { + delete(e.endpoints, path) + } + return nil +} diff --git a/restlike/types_test.go b/restlike/types_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7b5b1a9d5b1bdaebc556ee4521379e48478019cb --- /dev/null +++ b/restlike/types_test.go @@ -0,0 +1,47 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import "testing" + +// Full test for all add/get/remove cases +func TestEndpoints(t *testing.T) { + ep := &Endpoints{endpoints: make(map[URI]map[Method]Callback)} + cb := func(*Message) *Message { + return nil + } + + testPath := URI("test/path") + testMethod := Get + err := ep.Add(testPath, testMethod, cb) + if _, ok := ep.endpoints[testPath][testMethod]; err != nil || !ok { + t.Errorf("Failed to add endpoint: %+v", err) + } + err = ep.Add(testPath, testMethod, cb) + if _, ok := ep.endpoints[testPath][testMethod]; err == nil || !ok { + t.Errorf("Expected failure to add endpoint") + } + + resultCb, err := ep.Get(testPath, testMethod) + if resultCb == nil || err != nil { + t.Errorf("Expected to get endpoint: %+v", err) + } + + err = ep.Remove(testPath, testMethod) + if _, ok := ep.endpoints[testPath][testMethod]; err != nil || ok { + t.Errorf("Failed to remove endpoint: %+v", err) + } + err = ep.Remove(testPath, testMethod) + if _, ok := ep.endpoints[testPath][testMethod]; err == nil || ok { + t.Errorf("Expected failure to remove endpoint") + } + + resultCb, err = ep.Get(testPath, testMethod) + if resultCb != nil || err == nil { + t.Errorf("Expected failure to get endpoint: %+v", err) + } +} diff --git a/single/receivedRequest.go b/single/receivedRequest.go index 372557fbb0962bfcf3b9ddd4dffd8e747930b3e7..cb445e8b0a02b15d6458dd824a3cdd478d1167f3 100644 --- a/single/receivedRequest.go +++ b/single/receivedRequest.go @@ -18,6 +18,7 @@ import ( "gitlab.com/xx_network/primitives/id/ephemeral" "sync" "sync/atomic" + "testing" "time" ) @@ -223,3 +224,17 @@ func splitPayload(payload []byte, maxSize, maxParts int) [][]byte { } return parts } + +// BuildTestRequest can be used for mocking a Request +func BuildTestRequest(payload []byte, t *testing.T) *Request { + return &Request{ + sender: nil, + senderPubKey: nil, + dhKey: nil, + tag: "", + maxParts: 0, + used: nil, + requestPayload: payload, + net: nil, + } +}