diff --git a/cmd/main/node.go b/cmd/main/node.go index 0273168..28c0880 100644 --- a/cmd/main/node.go +++ b/cmd/main/node.go @@ -1,14 +1,13 @@ package main import ( - "github.com/akyaiy/GoSally-mvp/internal/config" - "github.com/akyaiy/GoSally-mvp/internal/logs" - "crypto/rand" - "encoding/hex" "log/slog" "net/http" "regexp" + "github.com/akyaiy/GoSally-mvp/internal/config" + "github.com/akyaiy/GoSally-mvp/internal/logs" + "github.com/go-chi/chi/v5" ) @@ -42,13 +41,3 @@ func main() { http.ListenAndServe(cfg.Address, r) } - -func newUUID() string { - bytes := make([]byte, 16) - _, err := rand.Read(bytes) - if err != nil { - log.Error("Failed to generate UUID", slog.String("error", err.Error())) - return "" - } - return hex.EncodeToString(bytes) -} diff --git a/internal/logs/go.mod b/internal/logs/go.mod new file mode 100644 index 0000000..c6aff4c --- /dev/null +++ b/internal/logs/go.mod @@ -0,0 +1,12 @@ +module github.com/akyaiy/GoSally-mvp/internal/logs + +go 1.24.4 + +require github.com/ilyakaznacheev/cleanenv v1.5.0 + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect +) diff --git a/internal/server/v1/go.mod b/internal/server/v1/go.mod index 200445e..4ff106c 100644 --- a/internal/server/v1/go.mod +++ b/internal/server/v1/go.mod @@ -3,12 +3,15 @@ module github.com/akyaiy/GoSally-mvp/internal/server_v1 go 1.24.4 require ( + github.com/akyaiy/GoSally-mvp/internal/config v0.0.0-20250622083853-07e1d3ecddf4 github.com/go-chi/chi/v5 v5.2.2 github.com/yuin/gopher-lua v1.1.1 ) require ( - github.com/gorilla/websocket v1.5.3 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect ) diff --git a/internal/server/v1/go.sum b/internal/server/v1/go.sum index 04a2df4..67e566e 100644 --- a/internal/server/v1/go.sum +++ b/internal/server/v1/go.sum @@ -1,11 +1,17 @@ -github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= -github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/akyaiy/GoSally-mvp/internal/config v0.0.0-20250622083853-07e1d3ecddf4 h1:pRcNlKaQUEvAjC9ideN8YZc2vCryTI6q045qf+NkfpI= +github.com/akyaiy/GoSally-mvp/internal/config v0.0.0-20250622083853-07e1d3ecddf4/go.mod h1:VCJJWOEkisTU5IBIuNVEc2ahosMRnoVy/I/Dnf79KVM= github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= +github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/server/v1/handle_com.go b/internal/server/v1/handle_com.go index 02b756e..45cc882 100644 --- a/internal/server/v1/handle_com.go +++ b/internal/server/v1/handle_com.go @@ -12,24 +12,24 @@ import ( ) func (h *HandlerV1) _handle() { - uuid16 := newUUID() - _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())) + 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())) cmd := chi.URLParam(h.r, "cmd") - if !allowedCmd.MatchString(string([]rune(cmd)[0])) { - writeJSONError(h.w, http.StatusBadRequest, "invalid command") - _log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "invalid command"), slog.String("cmd", cmd), slog.Int("status", http.StatusBadRequest)) + if !h.allowedCmd.MatchString(string([]rune(cmd)[0])) { + 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 !listAllowedCmd.MatchString(cmd) { - writeJSONError(h.w, http.StatusBadRequest, "invalid command") - _log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "invalid command"), slog.String("cmd", cmd), slog.Int("status", http.StatusBadRequest)) + if !h.listAllowedCmd.MatchString(cmd) { + 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(cfg.ComDir, cmd+".lua") + scriptPath := filepath.Join(h.cfg.ComDir, cmd+".lua") if _, err := os.Stat(scriptPath); err != nil { - writeJSONError(h.w, http.StatusNotFound, "command not found") - _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 } @@ -57,8 +57,8 @@ func (h *HandlerV1) _handle() { `) if err := L.DoFile(scriptPath); err != nil { - writeJSONError(h.w, http.StatusInternalServerError, "lua error: "+err.Error()) - _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 } @@ -78,15 +78,15 @@ func (h *HandlerV1) _handle() { }) } - w.Header().Set("Content-Type", "application/json") + h.w.Header().Set("Content-Type", "application/json") json.NewEncoder(h.w).Encode(out) switch out["status"] { case "error": - _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": - _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: - _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)) } - _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/internal/server/v1/handle_list.go b/internal/server/v1/handle_list.go index 62f34b7..65ce138 100644 --- a/internal/server/v1/handle_list.go +++ b/internal/server/v1/handle_list.go @@ -6,13 +6,13 @@ import ( "net/http" "os" "path/filepath" - "github.com/akyaiy/GoSally-mvp/internal/config" + _ "github.com/go-chi/chi/v5" ) func (h *HandlerV1) _handleList() { - uuid16 := newUUID() - _log.Info("Received request", slog.String("version", "v1"), slog.String("connection-uuid", uuid16), slog.String("remote", r.RemoteAddr), slog.String("method", r.Method), slog.String("url", r.URL.String())) + 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())) type ComMeta struct { Description string } @@ -23,9 +23,9 @@ func (h *HandlerV1) _handleList() { commands = make(map[string]ComMeta) ) - if files, err = os.ReadDir(cfg.ComDir); err != nil { - writeJSONError(w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error()) - _log.Error("Failed to read commands directory", slog.String("error", err.Error())) + if files, err = os.ReadDir(h.cfg.ComDir); err != nil { + 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 } for _, file := range files { @@ -33,15 +33,15 @@ func (h *HandlerV1) _handleList() { continue } cmdName := file.Name()[:len(file.Name())-4] // remove .lua extension - if !allowedCmd.MatchString(string([]rune(cmdName)[0])) { + if !h.allowedCmd.MatchString(string([]rune(cmdName)[0])) { continue } - if !listAllowedCmd.MatchString(cmdName) { + if !h.listAllowedCmd.MatchString(cmdName) { continue } - if com.Description, err = extractDescriptionStatic(filepath.Join(cfg.ComDir, file.Name())); err != nil { - writeJSONError(w, http.StatusInternalServerError, "failed to read command: "+err.Error()) - log.Error("Failed to read command", slog.String("error", err.Error())) + 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 com.Description == "" { @@ -49,7 +49,7 @@ func (h *HandlerV1) _handleList() { } commands[cmdName] = ComMeta{Description: com.Description} } - json.NewEncoder(w).Encode(commands) - _log.Info("Command executed successfully", slog.String("connection-uuid", uuid16)) - _log.Info("Session completed", slog.String("connection-uuid", uuid16), slog.String("remote", r.RemoteAddr), slog.String("method", r.Method), slog.String("url", r.URL.String())) + 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/internal/server/v1/server.go b/internal/server/v1/server.go index 02748e8..d030073 100644 --- a/internal/server/v1/server.go +++ b/internal/server/v1/server.go @@ -1,16 +1,23 @@ package server_v1 import ( - "encoding/json" "log/slog" "net/http" - "os" "regexp" - "GoSally-mvp/internal/config" + "github.com/akyaiy/GoSally-mvp/internal/config" ) +type ServerV1UtilsContract interface { + extractDescriptionStatic(path string) (string, error) + writeJSONError(status int, msg string) + newUUID() string + errNotFound() +} + type ServerV1Contract interface { + ServerV1UtilsContract + Handle(w http.ResponseWriter, r *http.Request) HandleList(w http.ResponseWriter, r *http.Request) @@ -22,11 +29,11 @@ type HandlerV1 struct { w http.ResponseWriter r *http.Request - _log slog.Logger + log slog.Logger cfg *config.ConfigConf - allowedCmd *regexp.Regexp + allowedCmd *regexp.Regexp listAllowedCmd *regexp.Regexp } @@ -41,33 +48,3 @@ func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) { h.r = r h._handleList() } - -func errNotFound(w http.ResponseWriter, r *http.Request) { - writeJSONError(w, http.StatusBadRequest, "invalid request") - _log.Error("HTTP request error", slog.String("remote", r.RemoteAddr), slog.String("method", r.Method), slog.String("url", r.URL.String()), slog.Int("status", http.StatusBadRequest)) -} - -func writeJSONError(w http.ResponseWriter, status int, msg string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - resp := map[string]interface{}{ - "status": "error", - "error": msg, - "code": status, - } - json.NewEncoder(w).Encode(resp) -} - -func 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 -} diff --git a/internal/server/v1/utils.go b/internal/server/v1/utils.go new file mode 100644 index 0000000..8b9be0c --- /dev/null +++ b/internal/server/v1/utils.go @@ -0,0 +1,51 @@ +package server_v1 + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "log/slog" + "net/http" + "os" + "regexp" +) + +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 +}