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