mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-11 02:48:21 +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
|
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) {
|
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