Refactor and restructure core components

- Removed obsolete files: `hanle_multi.go`, `logger.go`, `handle_com.go`, `handle_list.go`, `server.go`, and `utils.go` from the `general_server` and `sv1` packages.
- Introduced new implementations for the `GeneralServer` and `HandlerV1` in the `core` package, enhancing the overall architecture.
- Updated the `go.mod` and `go.sum` files to include new dependencies.
- Added a new configuration structure in `config.yaml` and `config.go` for better management of application settings.
- Implemented a Lua command handling mechanism in `handle_com.go` and `handle_list.go` for improved command execution.
- Enhanced logging capabilities with a new logger setup in `logger.go`.
- Added a new Lua script `http.lua` for handling HTTP requests.
This commit is contained in:
alex
2025-06-26 16:50:35 +03:00
parent 9f7f046a07
commit 96fb13e3c7
15 changed files with 214 additions and 145 deletions

58
core/config/config.go Normal file
View File

@@ -0,0 +1,58 @@
package config
import (
"log"
"os"
"time"
"github.com/ilyakaznacheev/cleanenv"
)
type ConfigConf struct {
Mode string `yaml:"mode" env-default:"dev"`
ComDir string `yaml:"com_dir" env-default:"./com/"`
HTTPServer `yaml:"http_server"`
TLS `yaml:"tls"`
}
type TLS struct {
TlsEnabled string `yaml:"enabled" env-default:"false"`
CertFile string `yaml:"cert_file" env-default:"./cert/server.crt"`
KeyFile string `yaml:"key_file" env-default:"./cert/server.key"`
}
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"`
HTTPServer_Api `yaml:"api"`
}
type HTTPServer_Api struct {
LatestVer string `yaml:"latest-version" env-required:"true"`
Layers []string `yaml:"layers"`
}
type ConfigEnv struct {
ConfigPath string `env:"CONFIG_PATH" env-default:"./cfg/config.yaml"`
}
func MustLoadConfig() *ConfigConf {
log.SetOutput(os.Stderr)
var configEnv ConfigEnv
if err := cleanenv.ReadEnv(&configEnv); err != nil {
log.Fatalf("Failed to read environment variables: %v", err)
os.Exit(1)
}
if _, err := os.Stat(configEnv.ConfigPath); os.IsNotExist(err) {
log.Fatalf("Config file does not exist: %s", configEnv.ConfigPath)
os.Exit(2)
}
var config ConfigConf
if err := cleanenv.ReadConfig(configEnv.ConfigPath, &config); err != nil {
log.Fatalf("Failed to read config file: %v", err)
os.Exit(3)
}
log.Printf("Configuration loaded successfully from %s", configEnv.ConfigPath)
return &config
}

View File

@@ -0,0 +1,164 @@
package general_server
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"slices"
"github.com/akyaiy/GoSally-mvp/core/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")
log := s.log.With(
slog.Group("request",
slog.String("version", serverReqApiVer),
slog.String("url", s.r.URL.String()),
slog.String("method", s.r.Method),
),
slog.Group("connection",
slog.String("remote", s.r.RemoteAddr),
),
)
s.log.Debug("Received request")
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.Debug("Using latest version under custom layer",
slog.String("layer", serverReqApiVer),
slog.String("fallback-version", s.cfg.LatestVer),
)
srv.Handle(w, r)
return
}
}
log.Error("HTTP request error: unsupported API version",
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")
log := s.log.With(
slog.Group("request",
slog.String("version", serverReqApiVer),
slog.String("url", s.r.URL.String()),
slog.String("method", s.r.Method),
),
slog.Group("connection",
slog.String("remote", s.r.RemoteAddr),
),
)
log.Debug("Received request")
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 {
log.Debug("Using latest version under custom layer",
slog.String("layer", serverReqApiVer),
slog.String("fallback-version", s.cfg.LatestVer),
)
srv.HandleList(w, r)
return
}
}
log.Error("HTTP request error: unsupported API version",
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)
}

25
core/logs/logger.go Normal file
View File

@@ -0,0 +1,25 @@
package logs
import (
"log/slog"
"os"
)
const (
envDev = "dev"
envProd = "prod"
)
func SetupLogger(env string) *slog.Logger {
var log *slog.Logger
switch env {
case envDev:
log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
case envProd:
log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
default:
log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
}
return log
}

174
core/sv1/handle_com.go Normal file
View File

@@ -0,0 +1,174 @@
package sv1
import (
"encoding/json"
"log/slog"
"net/http"
"os"
"path/filepath"
lua "github.com/aarzilli/golua/lua"
"github.com/go-chi/chi/v5"
)
func (h *HandlerV1) _handle() {
uuid16 := h.newUUID()
log := h.log.With(
slog.Group("request",
slog.String("version", h.GetVersion()),
slog.String("url", h.r.URL.String()),
slog.String("method", h.r.Method),
),
slog.Group("connection",
slog.String("connection-uuid", uuid16),
slog.String("remote", h.r.RemoteAddr),
),
)
log.Info("Received request")
cmd := chi.URLParam(h.r, "cmd")
var scriptPath string
if !h.allowedCmd.MatchString(string([]rune(cmd)[0])) {
log.Error("HTTP request error",
slog.String("error", "invalid command"),
slog.String("cmd", cmd),
slog.Int("status", http.StatusBadRequest))
h.writeJSONError(http.StatusBadRequest, "invalid command")
return
}
if !h.listAllowedCmd.MatchString(cmd) {
log.Error("HTTP request error",
slog.String("error", "invalid command"),
slog.String("cmd", cmd),
slog.Int("status", http.StatusBadRequest))
h.writeJSONError(http.StatusBadRequest, "invalid command")
return
}
if scriptPath = h.comMatch(chi.URLParam(h.r, "ver"), cmd); scriptPath == "" {
log.Error("HTTP request error",
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 {
log.Error("HTTP request error",
slog.String("error", "command not found"),
slog.String("cmd", cmd),
slog.Int("status", http.StatusNotFound))
h.writeJSONError(http.StatusNotFound, "command not found")
return
}
L := lua.NewState()
defer L.Close()
L.OpenLibs()
// Создаем таблицу Params
L.NewTable()
paramsTableIndex := L.GetTop() // Индекс таблицы в стеке
// Заполняем таблицу из query параметров
qt := h.r.URL.Query()
for k, v := range qt {
if len(v) > 0 {
L.PushString(v[0]) // Значение
L.SetField(paramsTableIndex, k) // paramsTable[k] = v[0]
}
}
// Помещаем Params в глобальные переменные
L.SetGlobal("Params")
// Создаем пустую таблицу Result
L.NewTable()
L.SetGlobal("Result")
// Загружаем и выполняем скрипт подготовки окружения, если есть
prepareLuaEnv := filepath.Join(h.cfg.ComDir, "_prepare.lua")
if _, err := os.Stat(prepareLuaEnv); err == nil {
if err := L.DoFile(prepareLuaEnv); err != nil {
log.Error("Failed to prepare lua environment",
slog.String("error", err.Error()))
h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error())
return
}
} else {
log.Error("No environment preparation script found, skipping preparation",
slog.String("error", err.Error()))
}
// Выполняем основной Lua скрипт
if err := L.DoFile(scriptPath); err != nil {
log.Error("Failed to execute lua script",
slog.Group("lua-status",
slog.String("error", err.Error()),
slog.String("lua-version", lua.LUA_VERSION)))
h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error())
return
}
// Получаем глобальную переменную Result (таблица)
L.GetGlobal("Result")
if L.IsTable(-1) {
out := make(map[string]interface{})
L.PushNil() // Первый ключ
for {
if L.Next(-2) == 0 {
break
}
// На стеке: -1 = value, -2 = key
key := L.ToString(-2)
var val interface{}
switch L.Type(-1) {
case lua.LUA_TSTRING:
val = L.ToString(-1)
case lua.LUA_TNUMBER:
val = L.ToNumber(-1)
case lua.LUA_TBOOLEAN:
val = L.ToBoolean(-1)
default:
// fallback
val = L.ToString(-1)
}
out[key] = val
L.Pop(1) // Удаляем value, key остаётся для следующего L.Next
}
L.Pop(1) // Удаляем таблицу Result со стека
h.w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(h.w).Encode(out); err != nil {
log.Error("Failed to encode JSON response",
slog.String("error", err.Error()))
}
switch out["status"] {
case "error":
log.Info("Command executed with error",
slog.String("cmd", cmd),
slog.Any("result", out))
case "ok":
log.Info("Command executed successfully",
slog.String("cmd", cmd), slog.Any("result", out))
default:
log.Info("Command executed and returned an unknown status",
slog.String("cmd", cmd),
slog.Any("result", out))
}
} else {
L.Pop(1) // убираем не таблицу из стека
log.Error("Lua global 'Result' is not a table")
h.writeJSONError(http.StatusInternalServerError, "'Result' is not a table")
return
}
log.Info("Session completed",
slog.Group("lua-status",
slog.String("lua-version", lua.LUA_VERSION)))
}

108
core/sv1/handle_list.go Normal file
View File

@@ -0,0 +1,108 @@
package sv1
import (
"encoding/json"
"log/slog"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/go-chi/chi/v5"
)
func (h *HandlerV1) _handleList() {
uuid16 := h.newUUID()
log := h.log.With(
slog.Group("request",
slog.String("version", h.GetVersion()),
slog.String("url", h.r.URL.String()),
slog.String("method", h.r.Method),
),
slog.Group("connection",
slog.String("connection-uuid", uuid16),
slog.String("remote", h.r.RemoteAddr),
),
)
log.Info("Received request")
type ComMeta struct {
Description string `json:"Description"`
Arguments map[string]string `json:"Arguments,omitempty"`
}
var (
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 {
log.Error("Failed to read commands directory",
slog.String("error", err.Error()))
h.writeJSONError(http.StatusInternalServerError, "failed to read commands directory: "+err.Error())
return
}
apiVer := chi.URLParam(h.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")
h.w.Header().Set("Content-Type", "application/json")
json.NewEncoder(h.w).Encode(commands)
}

67
core/sv1/server.go Normal file
View File

@@ -0,0 +1,67 @@
package sv1
import (
"log/slog"
"net/http"
"regexp"
"github.com/akyaiy/GoSally-mvp/core/config"
)
type ServerV1UtilsContract interface {
extractDescriptionStatic(path string) (string, error)
writeJSONError(status int, msg string)
newUUID() string
_errNotFound()
ErrNotFound(w http.ResponseWriter, r *http.Request)
}
// structure only for initialization
type HandlerV1InitStruct struct {
Ver string
Log slog.Logger
Config *config.ConfigConf
AllowedCmd *regexp.Regexp
ListAllowedCmd *regexp.Regexp
}
type HandlerV1 struct {
w http.ResponseWriter
r *http.Request
log slog.Logger
cfg *config.ConfigConf
allowedCmd *regexp.Regexp
listAllowedCmd *regexp.Regexp
ver string
}
func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 {
return &HandlerV1{
log: o.Log,
cfg: o.Config,
allowedCmd: o.AllowedCmd,
listAllowedCmd: o.ListAllowedCmd,
ver: o.Ver,
}
}
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
h.w = w
h.r = r
h._handle()
}
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
h.w = w
h.r = r
h._handleList()
}
func (h *HandlerV1) GetVersion() string {
return h.ver
}

92
core/sv1/utils.go Normal file
View File

@@ -0,0 +1,92 @@
package sv1
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log/slog"
"net/http"
"os"
"regexp"
)
func (h *HandlerV1) ErrNotFound(w http.ResponseWriter, r *http.Request) {
h.w = w
h.r = r
h._errNotFound()
}
func (h *HandlerV1) newUUID() string {
bytes := make([]byte, 16)
_, err := rand.Read(bytes)
if err != nil {
h.log.Error("Failed to generate UUID", slog.String("error", err.Error()))
return ""
}
return hex.EncodeToString(bytes)
}
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))
}
func (h *HandlerV1) writeJSONError(status int, msg string) {
h.w.Header().Set("Content-Type", "application/json")
h.w.WriteHeader(status)
resp := map[string]interface{}{
"status": "error",
"error": msg,
"code": status,
}
json.NewEncoder(h.w).Encode(resp)
}
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 {
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
}