mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-10 18:38:22 +00:00
Implement basic JsonRPC Handler
This commit is contained in:
parent
e36af35aad
commit
503464dbf1
8 changed files with 256 additions and 2 deletions
35
pkg/jsonrpc/error.go
Normal file
35
pkg/jsonrpc/error.go
Normal 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
72
pkg/jsonrpc/handler.go
Normal 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
10
pkg/jsonrpc/method.go
Normal 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
46
pkg/jsonrpc/register.go
Normal 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
11
pkg/jsonrpc/request.go
Normal 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
30
pkg/jsonrpc/response.go
Normal 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,
|
||||
})
|
||||
}
|
|
@ -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
35
pkg/server/jsonrpc.go
Normal 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
|
||||
}
|
Loading…
Add table
Reference in a new issue