From 9cdfdcd97189a5e3fbead43dc6d7b12815ba18f5 Mon Sep 17 00:00:00 2001 From: Jake Taylor <jake@elixxir.io> Date: Tue, 3 May 2022 16:35:13 -0500 Subject: [PATCH] added restlike package --- catalog/services.go | 2 + restlike/message.go | 52 ++++++++++++++++++++ restlike/receiver.go | 38 +++++++++++++++ restlike/request.go | 97 ++++++++++++++++++++++++++++++++++++ restlike/response.go | 40 +++++++++++++++ restlike/restServer.go | 67 +++++++++++++++++++++++++ restlike/types.go | 108 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 404 insertions(+) create mode 100644 restlike/message.go create mode 100644 restlike/receiver.go create mode 100644 restlike/request.go create mode 100644 restlike/response.go create mode 100644 restlike/restServer.go create mode 100644 restlike/types.go diff --git a/catalog/services.go b/catalog/services.go index cd28b663e..24ab74a46 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/message.go b/restlike/message.go new file mode 100644 index 000000000..cabcbe6bd --- /dev/null +++ b/restlike/message.go @@ -0,0 +1,52 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + "github.com/pkg/errors" +) + +// Message are used for sending to and receiving from a RestServer +type Message interface { + Content() Data + Headers() Param + Method() Method + URI() URI + Error() error +} + +// message implements the Message interface using JSON +type message struct { + content Data + headers Param + method Method + uri URI + err string +} + +func (m message) Content() Data { + return m.content +} + +func (m message) Headers() Param { + return m.headers +} + +func (m message) Method() Method { + return m.method +} + +func (m message) URI() URI { + return m.uri +} + +func (m message) Error() error { + if len(m.err) == 0 { + return nil + } + return errors.New(m.err) +} diff --git a/restlike/receiver.go b/restlike/receiver.go new file mode 100644 index 000000000..a3a80ef86 --- /dev/null +++ b/restlike/receiver.go @@ -0,0 +1,38 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + "encoding/json" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/single" +) + +// 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 +func (s *singleReceiver) Callback(req *single.Request, receptionId receptionID.EphemeralIdentity, rounds []rounds.Round) { + // Unmarshal the payload + newMessage := &message{} + err := json.Unmarshal(req.GetPayload(), newMessage) + if err != nil { + jww.ERROR.Printf("Unable to unmarshal restlike message: %+v", err) + return + } + + // Send the payload to the proper Callback + if cb, err := s.endpoints.Get(newMessage.URI(), newMessage.Method()); err == nil { + cb(newMessage) + } else { + jww.ERROR.Printf("Unable to call restlike endpoint: %+v", err) + } +} diff --git a/restlike/request.go b/restlike/request.go new file mode 100644 index 000000000..01cefd329 --- /dev/null +++ b/restlike/request.go @@ -0,0 +1,97 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + "encoding/json" + 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" +) + +// Request allows for making REST-like requests to a RestServer +type Request interface { + // Request provides several Method of sending Data to the given URI + // and blocks until the Message is returned + Request(method Method, recipient contact.Contact, path URI, content Data, param Param) (Message, error) + + // AsyncRequest provides several Method of sending Data to the given URI + // and will return the Message to the given Callback when received + AsyncRequest(method Method, recipient contact.Contact, path URI, content Data, param Param, cb Callback) error +} + +// SingleRequest implements the Request interface using single-use messages +// Can be used as stateful or declared inline without state +type SingleRequest struct { + RequestParam single.RequestParams + Net single.Cmix + Rng csprng.Source + E2eGrp *cyclic.Group + //func TransmitRequest(recipient contact.Contact, tag string, payload []byte, + // callback Response, param RequestParams, net Cmix, rng csprng.Source, + // e2eGrp *cyclic.Group) ([]id.Round, receptionID.EphemeralIdentity, error) +} + +// 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, param Param) (Message, error) { + // Build the Message + newMessage := &message{ + content: content, + headers: param, + method: method, + uri: path, + } + msg, err := json.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}, s.RequestParam, 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, param Param, cb Callback) error { + // Build the Message + newMessage := &message{ + content: content, + headers: param, + method: method, + uri: path, + } + msg, err := json.Marshal(newMessage) + if err != nil { + return err + } + + // Transmit the Message + _, _, err = single.TransmitRequest(recipient, catalog.RestLike, msg, + &singleResponse{responseCallback: cb}, s.RequestParam, s.Net, s.Rng, s.E2eGrp) + return err +} diff --git a/restlike/response.go b/restlike/response.go new file mode 100644 index 000000000..d8f4486c3 --- /dev/null +++ b/restlike/response.go @@ -0,0 +1,40 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import ( + "encoding/json" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" +) + +// processor is the response handler for a Request +type singleResponse struct { + responseCallback Callback +} + +// 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.err = err.Error() + s.responseCallback(newMessage) + } + + // Unmarshal the payload + err = json.Unmarshal(payload, newMessage) + if err != nil { + jww.ERROR.Printf("Unable to unmarshal restlike message: %+v", err) + return + } + + // Send the response payload to the responseCallback + s.responseCallback(newMessage) +} diff --git a/restlike/restServer.go b/restlike/restServer.go new file mode 100644 index 000000000..450c7ebfb --- /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: 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 = make(map[URI]map[Method]Callback) + // Destroy external services + r.listener.Stop() +} diff --git a/restlike/types.go b/restlike/types.go new file mode 100644 index 000000000..99c1791b2 --- /dev/null +++ b/restlike/types.go @@ -0,0 +1,108 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package restlike + +import "github.com/pkg/errors" + +// 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 string + +// Method defines the possible Request types +type Method uint8 + +// Callback provides the ability to make asynchronous Request +// in order to get the Message later without blocking +type Callback func(Message) + +// Param allows different configurations for each Request +// that will be specified in the Request header +type Param struct { + // Version allows for endpoints to be backwards-compatible + // and handle different formats of the same Request + Version uint + + // Headers allows for custom headers to be included with a Request + Headers Data +} + +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 map[URI]map[Method]Callback + +// Add a new Endpoint +// Returns an error if Endpoint already exists +func (e Endpoints) Add(path URI, method Method, cb Callback) error { + if _, ok := e[path]; !ok { + e[path] = make(map[Method]Callback) + } + if _, ok := e[path][method]; ok { + return errors.Errorf("unable to RegisterEndpoint: %s/%s already exists", path, method) + } + e[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) { + if _, ok := e[path]; !ok { + return nil, errors.Errorf("unable to locate endpoint: %s", path) + } + if _, innerOk := e[path][method]; !innerOk { + return nil, errors.Errorf("unable to locate endpoint: %s/%s", path, method) + } + return e[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()) + } + delete(e[path], method) + if len(e[path]) == 0 { + delete(e, path) + } + return nil +} -- GitLab