From 241809025d164bf8c63dd47bc716061dbf9d9a8a Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 23 Jun 2025 01:26:16 +0300 Subject: [PATCH] Enhance server functionality: add versioning support, implement command handling improvements, and introduce new Lua scripts for command execution --- cfg/config.yaml | 5 + cmd/node/node.go | 17 ++-- com/_globals.lua | 5 +- com/_prepare.lua | 5 + com/{echo?v2.lua => echo?b1.lua} | 2 +- com/{echo.lua => echo?s2.lua} | 0 config/config.go | 12 ++- general_server/hanle_multi.go | 159 +++++++++++++++++++++++++++++++ sv1/handle_com.go | 83 ++++++++++++---- sv1/handle_list.go | 89 +++++++++++++---- sv1/server.go | 18 ++-- sv1/utils.go | 37 ++++++- 12 files changed, 369 insertions(+), 63 deletions(-) create mode 100644 com/_prepare.lua rename com/{echo?v2.lua => echo?b1.lua} (93%) rename com/{echo.lua => echo?s2.lua} (100%) create mode 100644 general_server/hanle_multi.go diff --git a/cfg/config.yaml b/cfg/config.yaml index a52ae97..31580b3 100644 --- a/cfg/config.yaml +++ b/cfg/config.yaml @@ -4,5 +4,10 @@ http_server: address: "localhost:8080" timeout: 3s idle_timeout: 30s + api: + latest-version: v1 + layers: + - b1 + - s2 com_dir: "com/" \ No newline at end of file diff --git a/cmd/node/node.go b/cmd/node/node.go index 8782fe4..da5c668 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -6,6 +6,7 @@ import ( "regexp" "github.com/akyaiy/GoSally-mvp/config" + gs "github.com/akyaiy/GoSally-mvp/general_server" "github.com/akyaiy/GoSally-mvp/logs" "github.com/akyaiy/GoSally-mvp/sv1" @@ -31,18 +32,18 @@ func main() { Config: cfg, AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+$`), ListAllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9_-]+$`), + Ver: "v1", }) + s := gs.InitGeneral(&gs.GeneralServerInit{ + Log: *logs.SetupLogger(cfg.Mode), + Config: cfg, + }, serverv1) r := chi.NewRouter() - r.Route("/v1/com", func(r chi.Router) { - r.Get("/", serverv1.HandleList) - r.Get("/{cmd}", serverv1.Handle) + r.Route("/{ver}/com", func(r chi.Router) { + r.Get("/", s.HandleList) + r.Get("/{cmd}", s.Handle) }) - // r.Route("/v2/com", func(r chi.Router) { - // r.Get("/", handleV1ComList) - // r.Get("/{cmd}", handleV1) - // }) r.NotFound(serverv1.ErrNotFound) log.Info("Server started", slog.String("address", cfg.Address)) http.ListenAndServe(cfg.Address, r) - } diff --git a/com/_globals.lua b/com/_globals.lua index 9cab24c..611f429 100644 --- a/com/_globals.lua +++ b/com/_globals.lua @@ -4,7 +4,4 @@ Params = {} ---@type AnyTable -Result = {} - ----@type AnyTable -Me = {} \ No newline at end of file +Result = {} \ No newline at end of file diff --git a/com/_prepare.lua b/com/_prepare.lua new file mode 100644 index 0000000..5a2ce63 --- /dev/null +++ b/com/_prepare.lua @@ -0,0 +1,5 @@ +print = function() end +io.write = function(...) end +io.stdout = function() return nil end +io.stderr = function() return nil end +io.read = function(...) return nil end \ No newline at end of file diff --git a/com/echo?v2.lua b/com/echo?b1.lua similarity index 93% rename from com/echo?v2.lua rename to com/echo?b1.lua index 5f2dadc..5cb7b41 100644 --- a/com/echo?v2.lua +++ b/com/echo?b1.lua @@ -1,4 +1,4 @@ ---- #description = "Echoes back the message provided in the 'msg' parameter." +--- #description = "Echoes back the message provided in the 'msg' parameter. b1" if not Params.msg then Result.status = "error" diff --git a/com/echo.lua b/com/echo?s2.lua similarity index 100% rename from com/echo.lua rename to com/echo?s2.lua diff --git a/config/config.go b/config/config.go index 4cc15d7..77abccd 100644 --- a/config/config.go +++ b/config/config.go @@ -15,9 +15,15 @@ type ConfigConf struct { } type HTTPServer struct { - Address string `yaml:"address" env-default:"0.0.0.0:8080"` - Timeout time.Duration `yaml:"timeout" env-default:"5s"` - IdleTimeout time.Duration `yaml:"idle_timeout" env-default:"60s"` + Address string `yaml:"address" env-default:"0.0.0.0:8080"` + Timeout time.Duration `yaml:"timeout" env-default:"5s"` + IdleTimeout time.Duration `yaml:"idle_timeout" env-default:"60s"` + HTTPServer_Api `yaml:"api"` +} + +type HTTPServer_Api struct { + LatestVer string `yaml:"latest-version" env-required:"true"` + Layers []string `yaml:"layers"` } type ConfigEnv struct { diff --git a/general_server/hanle_multi.go b/general_server/hanle_multi.go new file mode 100644 index 0000000..ca20f89 --- /dev/null +++ b/general_server/hanle_multi.go @@ -0,0 +1,159 @@ +package general_server + +import ( + "encoding/json" + "errors" + "log/slog" + "net/http" + "slices" + + "github.com/akyaiy/GoSally-mvp/config" + + "github.com/go-chi/chi/v5" +) + +type serversApiVer string + +type GeneralServerApiContract interface { + GetVersion() string + + Handle(w http.ResponseWriter, r *http.Request) + HandleList(w http.ResponseWriter, r *http.Request) +} + +type GeneralServerContarct interface { + GeneralServerApiContract + AppendToArray(GeneralServerApiContract) error +} + +type GeneralServer struct { + w http.ResponseWriter + r *http.Request + + servers map[serversApiVer]GeneralServerApiContract + + log slog.Logger + cfg *config.ConfigConf +} + +// structure only for initialization +type GeneralServerInit struct { + Log slog.Logger + Config *config.ConfigConf +} + +func InitGeneral(o *GeneralServerInit, servers ...GeneralServerApiContract) *GeneralServer { + general := &GeneralServer{ + servers: make(map[serversApiVer]GeneralServerApiContract), + cfg: o.Config, + log: o.Log, + } + for _, s := range servers { + general.servers[serversApiVer(s.GetVersion())] = s + } + return general +} + +func (s *GeneralServer) GetVersion() string { + return "general" +} + +func (s *GeneralServer) AppendToArray(server GeneralServerApiContract) error { + if _, exist := s.servers[serversApiVer(server.GetVersion())]; !exist { + s.servers[serversApiVer(server.GetVersion())] = server + return nil + } + return errors.New("server with this version is already exist") +} + +func (s *GeneralServer) Handle(w http.ResponseWriter, r *http.Request) { + s.w = w + s.r = r + serverReqApiVer := chi.URLParam(r, "ver") + + s.log.Info("Received request", + slog.String("remote", r.RemoteAddr), + slog.String("method", r.Method), + slog.String("url", r.URL.String()), + slog.String("requested-version", serverReqApiVer), + ) + + if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok { + srv.Handle(w, r) + return + } + + if slices.Contains(s.cfg.Layers, serverReqApiVer) { + if srv, ok := s.servers[serversApiVer(s.cfg.LatestVer)]; ok { + s.log.Info("Using latest version under custom layer", + slog.String("layer", serverReqApiVer), + slog.String("fallback-version", s.cfg.LatestVer), + ) + srv.Handle(w, r) + return + } + } + + s.log.Error("HTTP request error: unsupported API version", + slog.String("remote", s.r.RemoteAddr), + slog.String("method", s.r.Method), + slog.String("url", s.r.URL.String()), + slog.Int("status", http.StatusBadRequest)) + s.writeJSONError(http.StatusBadRequest, "unsupported API version") +} + +func (s *GeneralServer) HandleList(w http.ResponseWriter, r *http.Request) { + s.w = w + s.r = r + serverReqApiVer := chi.URLParam(r, "ver") + + s.log.Info("Received request", + slog.String("remote", r.RemoteAddr), + slog.String("method", r.Method), + slog.String("url", r.URL.String()), + slog.String("requested-version", serverReqApiVer), + ) + + if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok { + srv.HandleList(w, r) + return + } + + if slices.Contains(s.cfg.Layers, serverReqApiVer) { + if srv, ok := s.servers[serversApiVer(s.cfg.LatestVer)]; ok { + s.log.Info("Using latest version under custom layer", + slog.String("layer", serverReqApiVer), + slog.String("fallback-version", s.cfg.LatestVer), + ) + srv.HandleList(w, r) + return + } + } + + s.log.Error("HTTP request error: unsupported API version", + slog.String("remote", s.r.RemoteAddr), + slog.String("method", s.r.Method), + slog.String("url", s.r.URL.String()), + slog.Int("status", http.StatusBadRequest)) + s.writeJSONError(http.StatusBadRequest, "unsupported API version") +} + +func (s *GeneralServer) _errNotFound() { + s.writeJSONError(http.StatusBadRequest, "invalid request") + s.log.Error("HTTP request error", + slog.String("remote", s.r.RemoteAddr), + slog.String("method", s.r.Method), + slog.String("url", s.r.URL.String()), + slog.Int("status", http.StatusBadRequest)) +} + +func (s *GeneralServer) writeJSONError(status int, msg string) { + s.w.Header().Set("Content-Type", "application/json") + s.w.WriteHeader(status) + resp := map[string]interface{}{ + "status": "error", + "error": msg, + "code": status, + } + json.NewEncoder(s.w).Encode(resp) +} diff --git a/sv1/handle_com.go b/sv1/handle_com.go index 6cf0a24..f89912a 100644 --- a/sv1/handle_com.go +++ b/sv1/handle_com.go @@ -13,23 +13,51 @@ import ( func (h *HandlerV1) _handle() { uuid16 := h.newUUID() - h.log.Info("Received request", slog.String("version", "v1"), slog.String("connection-uuid", uuid16), slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String())) + h.log.Info("Received request", + slog.String("version", "v1"), + slog.String("connection-uuid", uuid16), + slog.String("remote", h.r.RemoteAddr), + slog.String("method", h.r.Method), + slog.String("url", h.r.URL.String())) cmd := chi.URLParam(h.r, "cmd") + var scriptPath string if !h.allowedCmd.MatchString(string([]rune(cmd)[0])) { + h.log.Error("HTTP request error", + slog.String("connection-uuid", uuid16), + slog.String("error", "invalid command"), + slog.String("cmd", cmd), + slog.Int("status", http.StatusBadRequest)) h.writeJSONError(http.StatusBadRequest, "invalid command") - h.log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "invalid command"), slog.String("cmd", cmd), slog.Int("status", http.StatusBadRequest)) return } if !h.listAllowedCmd.MatchString(cmd) { + h.log.Error("HTTP request error", + slog.String("connection-uuid", uuid16), + slog.String("error", "invalid command"), + slog.String("cmd", cmd), + slog.Int("status", http.StatusBadRequest)) h.writeJSONError(http.StatusBadRequest, "invalid command") - h.log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "invalid command"), slog.String("cmd", cmd), slog.Int("status", http.StatusBadRequest)) return } - scriptPath := filepath.Join(h.cfg.ComDir, cmd+".lua") - if _, err := os.Stat(scriptPath); err != nil { + if scriptPath = h.comMatch(chi.URLParam(h.r, "ver"), cmd); scriptPath == "" { + h.log.Error("HTTP request error", + slog.String("connection-uuid", uuid16), + slog.String("error", "command not found"), + slog.String("cmd", cmd), + slog.Int("status", http.StatusNotFound)) + h.writeJSONError(http.StatusNotFound, "command not found") + return + } + + scriptPath = filepath.Join(h.cfg.ComDir, scriptPath) + if _, err := os.Stat(scriptPath); err != nil { + h.log.Error("HTTP request error", + slog.String("connection-uuid", uuid16), + slog.String("error", "command not found"), + slog.String("cmd", cmd), + slog.Int("status", http.StatusNotFound)) h.writeJSONError(http.StatusNotFound, "command not found") - h.log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "command not found"), slog.String("cmd", cmd), slog.Int("status", http.StatusNotFound)) return } @@ -48,17 +76,24 @@ func (h *HandlerV1) _handle() { L.SetGlobal("Params", tbl) L.SetGlobal("Result", L.NewTable()) - L.DoString(` - print = function() end - io.write = function(...) end - io.stdout = function() return nil end - io.stderr = function() return nil end - io.read = function(...) return nil end - `) + prepareLuaEnv := filepath.Join(h.cfg.ComDir, "_prepare"+".lua") + if _, err := os.Stat(prepareLuaEnv); err == nil { + if err := L.DoFile(prepareLuaEnv); err != nil { + h.log.Error("Failed to prepare lua environment", + slog.String("connection-uuid", uuid16), + slog.String("error", err.Error())) + h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error()) + return + } + } else { + h.log.Error("No environment preparation script found, skipping preparation", slog.String("connection-uuid", uuid16), slog.String("error", err.Error())) + } if err := L.DoFile(scriptPath); err != nil { + h.log.Error("Failed to execute lua script", + slog.String("connection-uuid", uuid16), + slog.String("error", err.Error())) h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error()) - h.log.Error("Failed to execute lua script", slog.String("connection-uuid", uuid16), slog.String("error", err.Error())) return } @@ -82,11 +117,23 @@ func (h *HandlerV1) _handle() { json.NewEncoder(h.w).Encode(out) switch out["status"] { case "error": - h.log.Info("Command executed with error", slog.String("connection-uuid", uuid16), slog.String("cmd", cmd), slog.Any("result", out)) + h.log.Info("Command executed with error", + slog.String("connection-uuid", uuid16), + slog.String("cmd", cmd), + slog.Any("result", out)) case "ok": - h.log.Info("Command executed successfully", slog.String("connection-uuid", uuid16), slog.String("cmd", cmd), slog.Any("result", out)) + h.log.Info("Command executed successfully", + slog.String("connection-uuid", uuid16), + slog.String("cmd", cmd), slog.Any("result", out)) default: - h.log.Info("Command executed and returned an unknown status", slog.String("connection-uuid", uuid16), slog.String("cmd", cmd), slog.Any("result", out)) + h.log.Info("Command executed and returned an unknown status", + slog.String("connection-uuid", uuid16), + slog.String("cmd", cmd), + slog.Any("result", out)) } - h.log.Info("Session completed", slog.String("connection-uuid", uuid16), slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String())) + h.log.Info("Session completed", + slog.String("connection-uuid", uuid16), + slog.String("remote", h.r.RemoteAddr), + slog.String("method", h.r.Method), + slog.String("url", h.r.URL.String())) } diff --git a/sv1/handle_list.go b/sv1/handle_list.go index 54f31ca..c52305f 100644 --- a/sv1/handle_list.go +++ b/sv1/handle_list.go @@ -6,50 +6,103 @@ import ( "net/http" "os" "path/filepath" + "strings" - _ "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5" ) func (h *HandlerV1) _handleList() { uuid16 := h.newUUID() - h.log.Info("Received request", slog.String("version", "v1"), slog.String("connection-uuid", uuid16), slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String())) + h.log.Info("Received request", + slog.String("version", "v1"), + slog.String("connection-uuid", uuid16), + slog.String("remote", h.r.RemoteAddr), + slog.String("method", h.r.Method), + slog.String("url", h.r.URL.String())) + type ComMeta struct { Description string } + var ( - files []os.DirEntry - err error - com ComMeta - commands = make(map[string]ComMeta) + files []os.DirEntry + err error + commands = make(map[string]ComMeta) + cmdsProcessed = make(map[string]bool) ) if files, err = os.ReadDir(h.cfg.ComDir); err != nil { + h.log.Error("Failed to read commands directory", + slog.String("error", err.Error())) h.writeJSONError(http.StatusInternalServerError, "failed to read commands directory: "+err.Error()) - h.log.Error("Failed to read commands directory", slog.String("error", err.Error())) return } + + apiVer := chi.URLParam(h.r, "ver") + + // Сначала ищем версионные for _, file := range files { if file.IsDir() || filepath.Ext(file.Name()) != ".lua" { continue } - cmdName := file.Name()[:len(file.Name())-4] // remove .lua extension + 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 com.Description, err = h.extractDescriptionStatic(filepath.Join(h.cfg.ComDir, file.Name())); err != nil { - h.writeJSONError(http.StatusInternalServerError, "failed to read command: "+err.Error()) - h.log.Error("Failed to read command", slog.String("error", err.Error())) - return + + 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 } - if com.Description == "" { - com.Description = "description missing" - } - commands[cmdName] = ComMeta{Description: com.Description} } + + // Потом фоллбеки + 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 + } + } + + h.log.Info("Command list prepared", + slog.String("connection-uuid", uuid16)) + + h.log.Info("Session completed", + slog.String("connection-uuid", uuid16), + slog.String("remote", h.r.RemoteAddr), + slog.String("method", h.r.Method), + slog.String("url", h.r.URL.String())) + + h.w.Header().Set("Content-Type", "application/json") json.NewEncoder(h.w).Encode(commands) - h.log.Info("Command executed successfully", slog.String("connection-uuid", uuid16)) - h.log.Info("Session completed", slog.String("connection-uuid", uuid16), slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String())) } diff --git a/sv1/server.go b/sv1/server.go index ddc49fc..aa3473d 100644 --- a/sv1/server.go +++ b/sv1/server.go @@ -17,18 +17,9 @@ type ServerV1UtilsContract interface { ErrNotFound(w http.ResponseWriter, r *http.Request) } -type ServerV1Contract interface { - ServerV1UtilsContract - - Handle(w http.ResponseWriter, r *http.Request) - HandleList(w http.ResponseWriter, r *http.Request) - - _handle() - _handleList() -} - // structure only for initialization type HandlerV1InitStruct struct { + Ver string Log slog.Logger Config *config.ConfigConf AllowedCmd *regexp.Regexp @@ -45,6 +36,8 @@ type HandlerV1 struct { allowedCmd *regexp.Regexp listAllowedCmd *regexp.Regexp + + ver string } func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 { @@ -53,6 +46,7 @@ func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 { cfg: o.Config, allowedCmd: o.AllowedCmd, listAllowedCmd: o.ListAllowedCmd, + ver: o.Ver, } } @@ -67,3 +61,7 @@ func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) { h.r = r h._handleList() } + +func (h *HandlerV1) GetVersion() string { + return h.ver +} diff --git a/sv1/utils.go b/sv1/utils.go index 6fe38a9..ba4d67a 100644 --- a/sv1/utils.go +++ b/sv1/utils.go @@ -28,7 +28,11 @@ func (h *HandlerV1) newUUID() string { func (h *HandlerV1) _errNotFound() { h.writeJSONError(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)) + 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) writeJSONError(status int, msg string) { @@ -55,3 +59,34 @@ func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) { } return m[1], nil } + +func (h *HandlerV1) comMatch(ver string, comName string) string { + files, err := os.ReadDir(h.cfg.ComDir) + if err != nil { + h.log.Error("Failed to read com dir", + slog.String("error", err.Error())) + return "" + } + + baseName := comName + ".lua" + verName := comName + "?" + ver + ".lua" + + var baseFileFound string + + for _, f := range files { + if f.IsDir() { + continue + } + fname := f.Name() + + if fname == verName { + return fname + } + + if fname == baseName { + baseFileFound = fname + } + } + + return baseFileFound +}