Skip to content

Commit

Permalink
feat(core): add access layer for get bridge api
Browse files Browse the repository at this point in the history
  • Loading branch information
shivanshkc committed Jul 3, 2022
1 parent 1c0d1e2 commit 6cbb33a
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 2 deletions.
2 changes: 2 additions & 0 deletions configs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
application:
name: rosenbridge
version: 0.0.0
bridge_limit_per_client: 10
bridge_limit_total: 100000

auth:
cluster_username: dev
Expand Down
5 changes: 3 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/shivanshkc/rosenbridge/src/cluster"
"github.com/shivanshkc/rosenbridge/src/configs"
"github.com/shivanshkc/rosenbridge/src/core"
"github.com/shivanshkc/rosenbridge/src/handlers"
"github.com/shivanshkc/rosenbridge/src/logger"
"github.com/shivanshkc/rosenbridge/src/messages"
"github.com/shivanshkc/rosenbridge/src/middlewares"
Expand Down Expand Up @@ -95,10 +96,10 @@ func getRouter() http.Handler {
// Internal routes require basic auth.
internalRouter.Use(middlewares.InternalBasicAuth)

externalRouter.HandleFunc("/", nil).
externalRouter.HandleFunc("", handlers.BasicHandler).
Methods(http.MethodGet, http.MethodOptions)

externalRouter.HandleFunc("/bridge", nil).
externalRouter.HandleFunc("/bridge", handlers.GetBridgeHandler).
Methods(http.MethodGet, http.MethodOptions)

externalRouter.HandleFunc("/message", nil).
Expand Down
6 changes: 6 additions & 0 deletions src/configs/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ type Model struct {
Name string `mapstructure:"name"`
// Version of the application.
Version string `mapstructure:"version"`
// BridgeLimitPerClient is the max number of bridges a client can have per node.
BridgeLimitPerClient int `mapstructure:"bridge_limit_per_client"`
// BridgeLimitTotal is the max number of bridges a node can host.
BridgeLimitTotal int `mapstructure:"bridge_limit_total"`
} `mapstructure:"application"`

// Auth is the model of authentication configs.
Auth struct {
// ClusterUsername is the username for internal basic auth.
ClusterUsername string `mapstructure:"cluster_username"`
// ClusterPassword is the password for internal basic auth.
ClusterPassword string `mapstructure:"cluster_password"`
} `mapstructure:"auth"`

Expand Down
26 changes: 26 additions & 0 deletions src/handlers/handler_basic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package handlers

import (
"net/http"

"github.com/shivanshkc/rosenbridge/src/configs"
"github.com/shivanshkc/rosenbridge/src/core"
"github.com/shivanshkc/rosenbridge/src/logger"
"github.com/shivanshkc/rosenbridge/src/utils/httputils"
)

// BasicHandler serves the base route, which is usually "/api".
func BasicHandler(writer http.ResponseWriter, req *http.Request) {
// Prerequisites.
ctx, conf, log := req.Context(), configs.Get(), logger.Get()

// Response body.
body := map[string]interface{}{
"code": core.CodeOK,
"name": conf.Application.Name,
"version": conf.Application.Version,
}

response := &httputils.ResponseDTO{Status: http.StatusOK, Body: body}
httputils.WriteAndLog(ctx, writer, response, log)
}
120 changes: 120 additions & 0 deletions src/handlers/handlers_get_bridge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package handlers

import (
"context"
"fmt"
"net/http"

"github.com/shivanshkc/rosenbridge/src/configs"
"github.com/shivanshkc/rosenbridge/src/core"
"github.com/shivanshkc/rosenbridge/src/logger"
"github.com/shivanshkc/rosenbridge/src/utils/errutils"
"github.com/shivanshkc/rosenbridge/src/utils/httputils"

"github.com/gorilla/mux"
)

// GetBridgeHandler serves the "Get Bridge" API of Rosenbridge.
func GetBridgeHandler(writer http.ResponseWriter, req *http.Request) {
// Prerequisites.
ctx, conf, log := req.Context(), configs.Get(), logger.Get()

// Reading and validating client ID.
clientID := mux.Vars(req)["client_id"]
if err := checkClientID(clientID); err != nil {
httputils.WriteErrAndLog(ctx, writer, errutils.BadRequest().WithReasonError(err), log)
return
}

// Input for the core function.
createBridgeParams := &core.CreateBridgeParams{
ClientID: clientID,
Writer: writer,
Request: req,
BridgeLimitTotal: &conf.Application.BridgeLimitTotal,
BridgeLimitPerClient: &conf.Application.BridgeLimitPerClient,
}

// Calling the core function and obtaining the bridge.
bridge, err := core.CreateBridge(ctx, createBridgeParams)
if err != nil {
log.Error(ctx, &logger.Entry{Payload: fmt.Errorf("error in core.GetBridge call: %w", err)})
httputils.WriteErrAndLog(ctx, writer, err, log)
return
}

// Setting up the message handler for the bridge.
bridge.SetMessageHandler(func(req *core.BridgeMessage) {
ctx := context.Background()

// Creating the bridge message. This will be the reply to the client.
bMessage := &core.BridgeMessage{
Type: core.MessageOutgoingRes,
RequestID: req.RequestID,
}

// This method will help send the validation errors through the bridge.
sendValidationErr := func(err error) {
errHTTP := errutils.BadRequest().WithReasonError(err)
// Using the error code and reason as the message body.
bMessage.Body = &core.CodeAndReason{Code: errHTTP.Code, Reason: errHTTP.Reason}
// Sending back the error.
if err := bridge.SendMessage(ctx, bMessage); err != nil {
log.Error(ctx, &logger.Entry{Payload: fmt.Errorf("error in bridge.SendMessage call: %w", err)})
}
}

// Validating the bridge message.
if err := checkBridgeMessage(req); err != nil {
sendValidationErr(err)
return
}

// Getting the OutgoingMessageReq from the message.
outMessageReq, err := interface2OutgoingMessageReq(req.Body)
if err != nil {
sendValidationErr(err)
return
}

// Validating the OutgoingMessageReq.
if err := checkOutgoingMessageReq(outMessageReq); err != nil {
sendValidationErr(err)
return
}

// Forming the input for the core function.
input := &core.PostMessageParams{
OutgoingMessageReq: outMessageReq,
RequestID: req.RequestID,
ClientID: clientID,
}

// Calling the core function.
response, err := core.PostMessage(ctx, input)
if err != nil {
// Converting the error to HTTP error to get code and reason.
errHTTP := errutils.ToHTTPError(err)
// Using the error code and reason as the message body.
bMessage.Body = &core.CodeAndReason{Code: errHTTP.Code, Reason: errHTTP.Reason}

// Sending back the error.
if err := bridge.SendMessage(ctx, bMessage); err != nil {
log.Error(ctx, &logger.Entry{Payload: fmt.Errorf("error in bridge.SendMessage call: %w", err)})
}

// Logging the error at our end.
log.Error(ctx, &logger.Entry{Payload: fmt.Errorf("error in core.PostMessage call: %w", err)})
return
}

// Using the response as the message body.
bMessage.Body = response

// Sending back the response.
if err := bridge.SendMessage(ctx, bMessage); err != nil {
log.Error(ctx, &logger.Entry{Payload: fmt.Errorf("error in bridge.SendMessage call: %w", err)})
return
}
})
}
25 changes: 25 additions & 0 deletions src/handlers/helper_funcs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package handlers

import (
"encoding/json"
"fmt"

"github.com/shivanshkc/rosenbridge/src/core"
)

// interface2OutgoingMessageReq converts the provided interface value to *core.OutgoingMessageReq.
func interface2OutgoingMessageReq(value interface{}) (*core.OutgoingMessageReq, error) {
// Marshalling the value to byte slice for later unmarshalling.
valueBytes, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("error in json.Marshal call: %w", err)
}

// Unmarshalling the value bytes into a *core.OutgoingMessageReq type.
omr := &core.OutgoingMessageReq{}
if err := json.Unmarshal(valueBytes, omr); err != nil {
return nil, fmt.Errorf("error in json.Unmarshal call: %w", err)
}

return omr, nil
}
62 changes: 62 additions & 0 deletions src/handlers/validation_funcs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package handlers

import (
"github.com/shivanshkc/rosenbridge/src/core"
)

// checkClientID checks if the provided client ID is valid.
func checkClientID(clientID string) error {
clientIDLen := len(clientID)
// Validating the length of client ID.
if clientIDLen < clientIDMinLen || clientIDLen > clientIDMaxLen {
return errClientID
}
// Validating format of client ID.
if !clientIDRegexp.MatchString(clientID) {
return errClientID
}
return nil
}

// checkBridgeMessage checks if the provided *core.BridgeMessage is valid.
func checkBridgeMessage(message *core.BridgeMessage) error {
if message.Body == nil {
return errEmptyBridgeMessageBody
}
return nil
}

// checkOutgoingMessageReq validates an "Outgoing Message Request".
func checkOutgoingMessageReq(req *core.OutgoingMessageReq) error {
if err := checkReceiverIDs(req.ReceiverIDs); err != nil {
return err
}
if err := checkPersist(req.Persist); err != nil {
return err
}
// No error was found. Returning nil.
return nil
}

// checkReceiverIDs checks if the provided receiver IDs are all valid.
//
// Note that the Receiver ID is the same thing as Client ID.
func checkReceiverIDs(receiverIDs []string) error {
if len(receiverIDs) == 0 {
return errEmptyReceiverIDs
}
for _, rec := range receiverIDs {
if err := checkClientID(rec); err != nil {
return errReceiverID
}
}
return nil
}

// checkPersist checks if the provided "persist" value is valid.
func checkPersist(persist string) error {
if persist != core.PersistTrue && persist != core.PersistFalse && persist != core.PersistIfError {
return errPersist
}
return nil
}
36 changes: 36 additions & 0 deletions src/handlers/validation_params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package handlers

import (
"errors"
"fmt"
"regexp"

"github.com/shivanshkc/rosenbridge/src/core"
)

// Validation params.
const (
clientIDMinLen = 1
clientIDMaxLen = 100
)

// Validation params that can't be Go constants.
var (
clientIDRegexp = regexp.MustCompile("^[a-zA-Z0-9-@._]*$")
)

// All validation errors.
var (
errClientID = fmt.Errorf("client id length should be between %d and %d, and should match regex %s",
clientIDMinLen, clientIDMaxLen, clientIDRegexp.String())

errEmptyReceiverIDs = errors.New("receiver ids cannot be empty")

errReceiverID = fmt.Errorf("receiver id length should be between %d and %d, and should match regex %s",
clientIDMinLen, clientIDMaxLen, clientIDRegexp.String())

errPersist = fmt.Errorf("persist must be one of: %s, %s and %s",
core.PersistTrue, core.PersistFalse, core.PersistIfError)

errEmptyBridgeMessageBody = errors.New("bridge message body cannot be empty")
)

0 comments on commit 6cbb33a

Please sign in to comment.