diff --git a/pkg/jsonrpc/error.go b/pkg/jsonrpc/error.go new file mode 100644 index 0000000..2997716 --- /dev/null +++ b/pkg/jsonrpc/error.go @@ -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 +} diff --git a/pkg/jsonrpc/handler.go b/pkg/jsonrpc/handler.go new file mode 100644 index 0000000..27f6162 --- /dev/null +++ b/pkg/jsonrpc/handler.go @@ -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 +} diff --git a/pkg/jsonrpc/method.go b/pkg/jsonrpc/method.go new file mode 100644 index 0000000..f96ff40 --- /dev/null +++ b/pkg/jsonrpc/method.go @@ -0,0 +1,10 @@ +package jsonrpc + +import "reflect" + +type method struct { + subSystem reflect.Value + handlerFunc reflect.Value + inType reflect.Type + outType reflect.Type +} diff --git a/pkg/jsonrpc/register.go b/pkg/jsonrpc/register.go new file mode 100644 index 0000000..3abca51 --- /dev/null +++ b/pkg/jsonrpc/register.go @@ -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), + } + } +} diff --git a/pkg/jsonrpc/request.go b/pkg/jsonrpc/request.go new file mode 100644 index 0000000..102f120 --- /dev/null +++ b/pkg/jsonrpc/request.go @@ -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"` +} diff --git a/pkg/jsonrpc/response.go b/pkg/jsonrpc/response.go new file mode 100644 index 0000000..88a8b12 --- /dev/null +++ b/pkg/jsonrpc/response.go @@ -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, + }) +} diff --git a/pkg/server/api.go b/pkg/server/api.go index 0525b52..f9324d7 100644 --- a/pkg/server/api.go +++ b/pkg/server/api.go @@ -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) + } } diff --git a/pkg/server/jsonrpc.go b/pkg/server/jsonrpc.go new file mode 100644 index 0000000..2b46fe1 --- /dev/null +++ b/pkg/server/jsonrpc.go @@ -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 +}