Enhance server functionality: add versioning support, implement command handling improvements, and introduce new Lua scripts for command execution

This commit is contained in:
alex
2025-06-23 01:26:16 +03:00
parent 03195dca59
commit 241809025d
12 changed files with 369 additions and 63 deletions

View File

@@ -4,5 +4,10 @@ http_server:
address: "localhost:8080"
timeout: 3s
idle_timeout: 30s
api:
latest-version: v1
layers:
- b1
- s2
com_dir: "com/"

View File

@@ -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)
}

View File

@@ -5,6 +5,3 @@ Params = {}
---@type AnyTable
Result = {}
---@type AnyTable
Me = {}

5
com/_prepare.lua Normal file
View File

@@ -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

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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()))
}

View File

@@ -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()))
}

View File

@@ -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
}

View File

@@ -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
}