Compare commits

...

7 Commits

9 changed files with 102 additions and 264 deletions

View File

@@ -13,14 +13,11 @@ type serversApiVer string
type ServerApiContract interface { type ServerApiContract interface {
GetVersion() string GetVersion() string
Handle(w http.ResponseWriter, r *http.Request, req rpc.RPCRequest) Handle(r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse
} }
// GeneralServer implements the GeneralServerApiContract and serves as a router for different API versions. // GeneralServer implements the GeneralServerApiContract and serves as a router for different API versions.
type GatewayServer struct { type GatewayServer struct {
w http.ResponseWriter
r *http.Request
// servers holds the registered servers by their API version. // servers holds the registered servers by their API version.
// The key is the version string, and the value is the server implementing GeneralServerApi // The key is the version string, and the value is the server implementing GeneralServerApi
servers map[serversApiVer]ServerApiContract servers map[serversApiVer]ServerApiContract

View File

@@ -5,16 +5,17 @@ import (
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
"net/http/httptest" "sync"
"github.com/akyaiy/GoSally-mvp/internal/server/rpc" "github.com/akyaiy/GoSally-mvp/internal/server/rpc"
) )
func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) { func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
var req rpc.RPCRequest w.Header().Set("Content-Type", "application/json")
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
rpc.WriteRouterError(w, http.StatusBadRequest, &rpc.RPCError{ w.WriteHeader(http.StatusBadRequest)
rpc.WriteError(w, &rpc.RPCResponse{
JSONRPC: rpc.JSONRPCVersion, JSONRPC: rpc.JSONRPCVersion,
ID: nil, ID: nil,
Error: map[string]any{ Error: map[string]any{
@@ -26,55 +27,70 @@ func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
return return
} }
if err := json.Unmarshal(body, &req); err != nil { // determine if the JSON-RPC request is a batch
rpc.WriteRouterError(w, http.StatusBadRequest, &rpc.RPCError{ var batch []rpc.RPCRequest
JSONRPC: rpc.JSONRPCVersion, json.Unmarshal(body, &batch)
ID: nil, var single rpc.RPCRequest
Error: map[string]any{ if batch == nil {
"code": rpc.ErrParseError, if err := json.Unmarshal(body, &single); err != nil {
"message": rpc.ErrParseErrorS, w.WriteHeader(http.StatusBadRequest)
}, rpc.WriteError(w, &rpc.RPCResponse{
}) JSONRPC: rpc.JSONRPCVersion,
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrParseErrorS)) ID: nil,
Error: map[string]any{
"code": rpc.ErrParseError,
"message": rpc.ErrParseErrorS,
},
})
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrParseErrorS))
return
}
resp := gs.Route(r, &single)
rpc.WriteResponse(w, resp)
return return
} }
if req.JSONRPC != rpc.JSONRPCVersion { // handle batch
rpc.WriteRouterError(w, http.StatusBadRequest, &rpc.RPCError{ responses := make(chan rpc.RPCResponse, len(batch))
JSONRPC: rpc.JSONRPCVersion, var wg sync.WaitGroup
ID: req.ID, for _, m := range batch {
Error: map[string]any{ wg.Add(1)
"code": rpc.ErrInvalidRequest, go func(req rpc.RPCRequest) {
"message": rpc.ErrInvalidRequestS, defer wg.Done()
}, res := gs.Route(r, &req)
}) if res != nil {
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrInvalidRequestS), slog.String("requested-version", req.JSONRPC)) responses <- *res
return }
}(m)
} }
wg.Wait()
close(responses)
gs.Route(w, r, req) var result []rpc.RPCResponse
for res := range responses {
result = append(result, res)
}
if len(result) > 0 {
json.NewEncoder(w).Encode(result)
}
} }
func (gs *GatewayServer) Route(w http.ResponseWriter, r *http.Request, req rpc.RPCRequest) { func (gs *GatewayServer) Route(r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse {
server, ok := gs.servers[serversApiVer(req.Params.ContextVersion)] if req.JSONRPC != rpc.JSONRPCVersion {
if !ok { gs.log.Info("invalid request received", slog.String("issue", rpc.ErrInvalidRequestS), slog.String("requested-version", req.JSONRPC))
rpc.WriteRouterError(w, http.StatusBadRequest, &rpc.RPCError{ return rpc.NewError(rpc.ErrInvalidRequest, rpc.ErrInvalidRequestS, req.ID)
JSONRPC: rpc.JSONRPCVersion,
ID: req.ID,
Error: map[string]any{
"code": rpc.ErrContextVersion,
"message": rpc.ErrContextVersionS,
},
})
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrContextVersionS), slog.String("requested-version", req.Params.ContextVersion))
return
} }
server, ok := gs.servers[serversApiVer(req.ContextVersion)]
if !ok {
gs.log.Info("invalid request received", slog.String("issue", rpc.ErrContextVersionS), slog.String("requested-version", req.ContextVersion))
return rpc.NewError(rpc.ErrContextVersion, rpc.ErrContextVersionS, req.ID)
}
resp := server.Handle(r, req)
// checks if request is notification // checks if request is notification
if req.ID == nil { if req.ID == nil {
rr := httptest.NewRecorder() return nil
server.Handle(rr, r, req)
return
} }
server.Handle(w, r, req) return resp
} }

View File

@@ -1,27 +1,20 @@
package rpc package rpc
type RPCRequest struct { import "encoding/json"
JSONRPC string `json:"jsonrpc"`
ID any `json:"id"`
Method string `json:"method"`
Params RPCRequestParams `json:"params"`
}
type RPCRequestParams struct { type RPCRequest struct {
ContextVersion string `json:"context-version"` JSONRPC string `json:"jsonrpc"`
Method map[string]any `json:"method-params"` ID *json.RawMessage `json:"id,omitempty"`
Method string `json:"method"`
Params any `json:"params,omitempty"`
ContextVersion string `json:"context-version,omitempty"`
} }
type RPCResponse struct { type RPCResponse struct {
JSONRPC string `json:"jsonrpc"` JSONRPC string `json:"jsonrpc"`
ID any `json:"id"` ID *json.RawMessage `json:"id"`
Result any `json:"result"` Result any `json:"result,omitempty"`
} Error any `json:"error,omitempty"`
type RPCError struct {
JSONRPC string `json:"jsonrpc"`
ID any `json:"id"`
Error any `json:"error"`
} }
const ( const (

View File

@@ -0,0 +1,22 @@
package rpc
import "encoding/json"
func NewError(code int, message string, id *json.RawMessage) *RPCResponse {
return &RPCResponse{
JSONRPC: JSONRPCVersion,
ID: id,
Error: map[string]any{
"code": code,
"message": message,
},
}
}
func NewResponse(result any, id *json.RawMessage) *RPCResponse {
return &RPCResponse{
JSONRPC: JSONRPCVersion,
ID: id,
Result: result,
}
}

View File

@@ -2,32 +2,22 @@ package rpc
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
) )
func write(w http.ResponseWriter, status int, msg any) error { func write(w http.ResponseWriter, msg *RPCResponse) error {
w.Header().Set("Content-Type", "application/json") data, err := json.Marshal(msg)
w.WriteHeader(status) if err != nil {
switch m := msg.(type) {
case RPCError, *RPCError,
RPCResponse, *RPCResponse:
data, err := json.Marshal(m)
if err != nil {
return err
}
_, err = w.Write(data)
return err return err
default:
return fmt.Errorf("invalid RPC structure: %T", msg)
} }
_, err = w.Write(data)
return err
} }
func WriteRouterError(w http.ResponseWriter, status int, errm *RPCError) error { func WriteError(w http.ResponseWriter, errm *RPCResponse) error {
return write(w, status, errm) return write(w, errm)
} }
func WriteResponse(w http.ResponseWriter, response *RPCResponse) error { func WriteResponse(w http.ResponseWriter, response *RPCResponse) error {
return write(w, http.StatusOK, response) return write(w, response)
} }

View File

@@ -6,12 +6,8 @@ import (
"github.com/akyaiy/GoSally-mvp/internal/server/rpc" "github.com/akyaiy/GoSally-mvp/internal/server/rpc"
) )
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request, req rpc.RPCRequest) { func (h *HandlerV1) Handle(r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse {
rpc.WriteResponse(w, &rpc.RPCResponse{ return rpc.NewResponse("Hi", req.ID) // test answer to make sure everything works
JSONRPC: rpc.JSONRPCVersion,
ID: req.ID,
Result: "Hi",
}) // test answer to make sure everything works
} }
// func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) { // func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {

View File

@@ -1,133 +0,0 @@
package sv1
/*
import (
"encoding/json"
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/akyaiy/GoSally-mvp/core/config"
"github.com/akyaiy/GoSally-mvp/core/corestate"
"github.com/akyaiy/GoSally-mvp/core/utils"
"github.com/go-chi/chi/v5"
)
// The function processes the HTTP request and returns a list of available commands.
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
uuid16, err := utils.NewUUID(int(config.UUIDLength))
if err != nil {
h.log.Error("Failed to generate UUID",
slog.String("error", err.Error()))
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error()); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
log := h.log.With(
slog.Group("request",
slog.String("version", h.GetVersion()),
slog.String("url", r.URL.String()),
slog.String("method", r.Method),
),
slog.Group("connection",
slog.String("connection-uuid", uuid16),
slog.String("remote", r.RemoteAddr),
),
)
log.Info("Received request")
type ComMeta struct {
Description string `json:"Description"`
Arguments map[string]string `json:"Arguments,omitempty"`
}
var (
files []os.DirEntry
commands = make(map[string]ComMeta)
cmdsProcessed = make(map[string]bool)
)
if files, err = os.ReadDir(h.cfg.ComDir); err != nil {
log.Error("Failed to read commands directory",
slog.String("error", err.Error()))
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error()); err != nil {
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
}
return
}
apiVer := chi.URLParam(r, "ver")
// Сначала ищем версионные
for _, file := range files {
if file.IsDir() || filepath.Ext(file.Name()) != ".lua" {
continue
}
cmdFull := file.Name()[:len(file.Name())-4]
cmdParts := strings.SplitN(cmdFull, "?", 2)
cmdName := cmdParts[0]
if !h.allowedCmd.MatchString(string([]rune(cmdName)[0])) {
continue
}
if !h.listAllowedCmd.MatchString(cmdName) {
continue
}
if len(cmdParts) == 2 && cmdParts[1] == apiVer {
description, _ := h.extractDescriptionStatic(filepath.Join(h.cfg.ComDir, file.Name()))
if description == "" {
description = "description missing"
}
commands[cmdName] = ComMeta{Description: description}
cmdsProcessed[cmdName] = true
}
}
// Потом фоллбеки
for _, file := range files {
if file.IsDir() || filepath.Ext(file.Name()) != ".lua" {
continue
}
cmdFull := file.Name()[:len(file.Name())-4]
cmdParts := strings.SplitN(cmdFull, "?", 2)
cmdName := cmdParts[0]
if !h.allowedCmd.MatchString(string([]rune(cmdName)[0])) {
continue
}
if !h.listAllowedCmd.MatchString(cmdName) {
continue
}
if cmdsProcessed[cmdName] {
continue
}
if len(cmdParts) == 1 {
description, _ := h.extractDescriptionStatic(filepath.Join(h.cfg.ComDir, file.Name()))
if description == "" {
description = "description missing"
}
commands[cmdName] = ComMeta{Description: description}
cmdsProcessed[cmdName] = true
}
}
log.Debug("Command list prepared")
log.Info("Session completed")
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.MetaDir, "uuid"))
response := ResponseFormat{
ResponsibleAgentUUID: uuid32,
RequestedCommand: "list",
Response: commands,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
h.log.Error("Failed to write JSON error response",
slog.String("error", err.Error()))
}
}
*/

View File

@@ -1,20 +0,0 @@
package sv1
// PETTI - Go Sally Protocol for Exchanging Technical Tasks and Information
type PettiRequest struct {
PettiVer string `json:"PettiVer"`
PackageType struct {
Request string `json:"Request"`
} `json:"PackageType"`
Payload map[string]any `json:"Payload"`
}
type PettiResponse struct {
PettiVer string `json:"PettiVer"`
PackageType struct {
AnswerOf string `json:"AnswerOf"`
} `json:"PackageType"`
ResponsibleAgentUUID string `json:"ResponsibleAgentUUID"`
Payload map[string]any `json:"Payload"`
}

View File

@@ -5,29 +5,6 @@ import (
"os" "os"
) )
// func (h *HandlerV1) errNotFound(w http.ResponseWriter, r *http.Request) {
// utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid request")
// h.log.Error("HTTP request error",
// slog.String("remote", h.r.RemoteAddr),
// slog.String("method", h.r.Method),
// slog.String("url", h.r.URL.String()),
// slog.Int("status", http.StatusBadRequest))
// }
// func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) {
// data, err := os.ReadFile(path)
// if err != nil {
// return "", err
// }
// re := regexp.MustCompile(`---\s*#description\s*=\s*"([^"]+)"`)
// m := re.FindStringSubmatch(string(data))
// if len(m) <= 0 {
// return "", nil
// }
// return m[1], nil
// }
func (h *HandlerV1) comMatch(ver string, comName string) string { func (h *HandlerV1) comMatch(ver string, comName string) string {
files, err := os.ReadDir(h.cfg.ComDir) files, err := os.ReadDir(h.cfg.ComDir)
if err != nil { if err != nil {