Implement basic JsonRPC Handler

This commit is contained in:
Samuel Lorch 2023-03-05 18:57:43 +01:00
parent e36af35aad
commit 503464dbf1
8 changed files with 256 additions and 2 deletions

35
pkg/jsonrpc/error.go Normal file
View file

@ -0,0 +1,35 @@
package jsonrpc
import (
"io"
)
type ErrorCode int
const (
ErrParse ErrorCode = -32700
ErrInvalidRequest ErrorCode = -32600
ErrMethodNotFound ErrorCode = -32601
ErrInvalidParams ErrorCode = -32602
ErrInternalError ErrorCode = -32603
// Custom
ErrRequestError ErrorCode = -32000
)
type respError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
}
func respondError(w io.Writer, id any, code ErrorCode, err error) error {
respond(w, response{
Jsonrpc: "2.0",
ID: id,
Error: &respError{
Code: code,
Message: err.Error(),
},
})
return err
}

72
pkg/jsonrpc/handler.go Normal file
View file

@ -0,0 +1,72 @@
package jsonrpc
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"reflect"
)
type Handler struct {
methods map[string]method
maxRequestSize int64
}
func NewHandler(maxRequestSize int64) Handler {
return Handler{
methods: map[string]method{},
maxRequestSize: maxRequestSize,
}
}
func (h *Handler) HandleRequest(ctx context.Context, r io.Reader, w io.Writer) error {
var req request
bufferedRequest := new(bytes.Buffer)
reqSize, err := bufferedRequest.ReadFrom(io.LimitReader(r, h.maxRequestSize+1))
if err != nil {
return respondError(w, "", ErrInternalError, fmt.Errorf("Reading Request: %w", err))
}
if reqSize > h.maxRequestSize {
return respondError(w, "", ErrParse, fmt.Errorf("Request exceeds Max Request Size"))
}
dec := json.NewDecoder(bufferedRequest)
dec.DisallowUnknownFields()
err = dec.Decode(&req)
if err != nil {
return respondError(w, "", ErrParse, fmt.Errorf("Parsing Request: %w", err))
}
method, ok := h.methods[req.Method]
if !ok {
return respondError(w, "", ErrMethodNotFound, fmt.Errorf("Unknown Method %v", req.Method))
}
p := reflect.New(method.inType)
paramPointer := p.Interface()
dec = json.NewDecoder(bytes.NewReader(req.Params))
dec.DisallowUnknownFields()
err = dec.Decode(paramPointer)
if err != nil {
return respondError(w, "", ErrInvalidParams, fmt.Errorf("Parsing Request: %w", err))
}
params := make([]reflect.Value, 3)
params[0] = method.subSystem
params[1] = reflect.ValueOf(ctx)
params[2] = reflect.ValueOf(paramPointer).Elem()
res := method.handlerFunc.Call(params)
result := res[0].Interface()
if !res[1].IsNil() {
reqerr := res[1].Interface().(error)
return respondError(w, req.ID, 0, reqerr)
}
respondResult(w, req.ID, result)
return nil
}

10
pkg/jsonrpc/method.go Normal file
View file

@ -0,0 +1,10 @@
package jsonrpc
import "reflect"
type method struct {
subSystem reflect.Value
handlerFunc reflect.Value
inType reflect.Type
outType reflect.Type
}

46
pkg/jsonrpc/register.go Normal file
View file

@ -0,0 +1,46 @@
package jsonrpc
import (
"context"
"fmt"
"reflect"
)
func (h *Handler) Register(subSystemName string, s any) {
subSystem := reflect.ValueOf(s)
for i := 0; i < subSystem.NumMethod(); i++ {
m := subSystem.Type().Method(i)
funcType := m.Func.Type()
if funcType.NumIn() != 3 {
panic(fmt.Errorf("2 parameters are required %v", funcType.NumIn()))
}
if funcType.In(1) != reflect.TypeOf(new(context.Context)).Elem() {
panic(fmt.Errorf("the first argument needs to be a context.Context instead of %v ", funcType.In(1)))
}
if funcType.In(2).Kind() != reflect.Struct {
panic("the second argument needs to be a struct")
}
if funcType.NumOut() != 2 {
panic("2 return types are required")
}
if reflect.TypeOf(new(error)).Implements(funcType.Out(1)) {
panic("the second return type needs to be a error")
}
name := m.Name
if subSystemName != "" {
name = subSystemName + "." + name
}
h.methods[name] = method{
handlerFunc: m.Func,
subSystem: subSystem,
inType: funcType.In(2),
outType: funcType.Out(0),
}
}
}

11
pkg/jsonrpc/request.go Normal file
View file

@ -0,0 +1,11 @@
package jsonrpc
import "encoding/json"
type request struct {
Jsonrpc string `json:"jsonrpc"`
ID any `json:"id,omitempty"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
Meta map[string]string `json:"meta,omitempty"`
}

30
pkg/jsonrpc/response.go Normal file
View file

@ -0,0 +1,30 @@
package jsonrpc
import (
"encoding/json"
"io"
"golang.org/x/exp/slog"
)
type response struct {
Jsonrpc string `json:"jsonrpc"`
Result any `json:"result,omitempty"`
ID any `json:"id"`
Error *respError `json:"error,omitempty"`
}
func respond(w io.Writer, resp response) {
err := json.NewEncoder(w).Encode(resp)
if err != nil {
slog.Warn("write response", err)
}
}
func respondResult(w io.Writer, id, res any) {
respond(w, response{
Jsonrpc: "2.0",
ID: id,
Result: res,
})
}

View file

@ -1,7 +1,22 @@
package server
import "net/http"
import (
"context"
"fmt"
"net/http"
"runtime/debug"
"golang.org/x/exp/slog"
)
func HandleAPI(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
slog.Error("Recovered Panic Handling HTTP API Request", fmt.Errorf("%v", r), "stack", debug.Stack())
}
}()
err := apiHandler.HandleRequest(context.TODO(), r.Body, w)
if err != nil {
slog.Error("Handling HTTP API Request", err)
}
}

35
pkg/server/jsonrpc.go Normal file
View file

@ -0,0 +1,35 @@
package server
import (
"context"
"fmt"
"nfsense.net/nfsense/pkg/jsonrpc"
)
var apiHandler jsonrpc.Handler
func init() {
apiHandler = jsonrpc.NewHandler(100 << 20)
apiHandler.Register("test", Ping{})
}
type Ping struct {
}
type PingRequest struct {
Msg string `json:"msg"`
}
type PingResponse struct {
Msg string `json:"msg"`
}
func (p Ping) Ping(ctx context.Context, req PingRequest) (*PingResponse, error) {
if req.Msg == "" {
return nil, fmt.Errorf("Message is empty")
}
return &PingResponse{
Msg: req.Msg,
}, nil
}