mirror of
https://github.com/akyaiy/GoSally-mvp.git
synced 2026-01-03 08:32:24 +00:00
first commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
bak/
|
||||||
|
bin/
|
||||||
7
.luarc.json
Normal file
7
.luarc.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"runtime.version": "Lua 5.1",
|
||||||
|
"workspace.library": [
|
||||||
|
"./scripts/_globals.lua"
|
||||||
|
],
|
||||||
|
"diagnostics.globals": ["Params", "Result"]
|
||||||
|
}
|
||||||
48
Makefile
Normal file
48
Makefile
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
APP_NAME := node
|
||||||
|
BIN_DIR := bin
|
||||||
|
GOPATH := $(shell go env GOPATH)
|
||||||
|
.PHONY: all build run test fmt vet lint check clean
|
||||||
|
|
||||||
|
all: build
|
||||||
|
|
||||||
|
lint-setup:
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
|
goimports-setup:
|
||||||
|
go install golang.org/x/tools/cmd/goimports@latest
|
||||||
|
|
||||||
|
golicenses-setup:
|
||||||
|
go install github.com/google/go-licenses@latest
|
||||||
|
|
||||||
|
setup: lint-setup goimports-setup golicenses-setup
|
||||||
|
@echo "Setting up the development environment..."
|
||||||
|
@mkdir -p $(BIN_DIR)
|
||||||
|
@echo "Setup complete. Run 'make build' to compile the application."
|
||||||
|
|
||||||
|
build:
|
||||||
|
@go build -o $(BIN_DIR)/$(APP_NAME) ./cmd/$(APP_NAME)
|
||||||
|
|
||||||
|
run: build
|
||||||
|
./$(BIN_DIR)/$(APP_NAME)
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go test ./... | grep -v '^?' || true
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@go fmt ./...
|
||||||
|
@$(GOPATH)/bin/goimports -w .
|
||||||
|
|
||||||
|
vet:
|
||||||
|
@go vet ./...
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@$(GOPATH)/bin/golangci-lint run
|
||||||
|
|
||||||
|
check: fmt vet lint test
|
||||||
|
|
||||||
|
licenses:
|
||||||
|
@$(GOPATH)/bin/go-licenses save ./... --save_path=third_party/licenses --force
|
||||||
|
@echo "Licenses have been exported to third_party/licenses"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rf bin
|
||||||
54
cmd/main/node.go
Normal file
54
cmd/main/node.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"GoSally-mvp/internal/config"
|
||||||
|
"GoSally-mvp/internal/logs"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var allowedCmd = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
|
||||||
|
var log *slog.Logger
|
||||||
|
var cfg *config.ConfigConf
|
||||||
|
var listAllowedCmd = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) // allowed symbols after first symbol
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cfg = config.MustLoadConfig()
|
||||||
|
|
||||||
|
log = logs.SetupLogger(cfg.Mode)
|
||||||
|
log = log.With("mode", cfg.Mode)
|
||||||
|
|
||||||
|
log.Info("Initializing server", slog.String("address", cfg.HTTPServer.Address))
|
||||||
|
log.Debug("Server running in debug mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Route("/v1/com", func(r chi.Router) {
|
||||||
|
r.Get("/", handleV1ComList)
|
||||||
|
r.Get("/{cmd}", handleV1)
|
||||||
|
})
|
||||||
|
r.Route("/v2/com", func(r chi.Router) {
|
||||||
|
r.Get("/", handleV1ComList)
|
||||||
|
r.Get("/{cmd}", handleV1)
|
||||||
|
})
|
||||||
|
r.NotFound(notFound)
|
||||||
|
log.Info("Server started", slog.String("address", cfg.Address))
|
||||||
|
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)
|
||||||
|
}
|
||||||
10
com/_globals.lua
Normal file
10
com/_globals.lua
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---@alias AnyTable table<string, any>
|
||||||
|
|
||||||
|
---@type AnyTable
|
||||||
|
Params = {}
|
||||||
|
|
||||||
|
---@type AnyTable
|
||||||
|
Result = {}
|
||||||
|
|
||||||
|
---@type AnyTable
|
||||||
|
Me = {}
|
||||||
11
com/echo.lua
Normal file
11
com/echo.lua
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
--- #description = "Echoes back the message provided in the 'msg' parameter."
|
||||||
|
|
||||||
|
if not Params.msg then
|
||||||
|
Result.status = "error"
|
||||||
|
Result.error = "Missing parameter: msg"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Result.status = "ok"
|
||||||
|
Result.answer = Params.msg
|
||||||
|
return
|
||||||
11
com/echo?v2.lua
Normal file
11
com/echo?v2.lua
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
--- #description = "Echoes back the message provided in the 'msg' parameter."
|
||||||
|
|
||||||
|
if not Params.msg then
|
||||||
|
Result.status = "error"
|
||||||
|
Result.error = "Missing parameter: msg"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
Result.status = "okv2"
|
||||||
|
Result.answer = Params.msg
|
||||||
|
return
|
||||||
16
com/exec.lua
Normal file
16
com/exec.lua
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
if not Params.f then
|
||||||
|
Result.status = "error"
|
||||||
|
Result.error = "Missing parameter: f"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local code = os.execute("touch " .. Params.f)
|
||||||
|
if code ~= 0 then
|
||||||
|
Result.status = "error"
|
||||||
|
Result.message = "Failed to execute command"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
Result.status = "ok"
|
||||||
|
Result.message = "Command executed successfully"
|
||||||
8
config/config.yaml
Normal file
8
config/config.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
mode: "dev"
|
||||||
|
|
||||||
|
http_server:
|
||||||
|
address: "localhost:8080"
|
||||||
|
timeout: 3s
|
||||||
|
idle_timeout: 30s
|
||||||
|
|
||||||
|
com_dir: "com/"
|
||||||
17
go.mod
Normal file
17
go.mod
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
module GoSally-mvp
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.2
|
||||||
|
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
|
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||||
|
)
|
||||||
20
go.sum
Normal file
20
go.sum
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
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/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=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||||
44
internal/config/config.go
Normal file
44
internal/config/config.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigEnv struct {
|
||||||
|
ConfigPath string `env:"CONFIG_PATH" env-default:"./config/config.yaml"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustLoadConfig() *ConfigConf {
|
||||||
|
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
|
||||||
|
}
|
||||||
25
internal/logs/logger.go
Normal file
25
internal/logs/logger.go
Normal 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
|
||||||
|
}
|
||||||
14
internal/server/v1/go.mod
Normal file
14
internal/server/v1/go.mod
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module server_v1
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
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
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
|
||||||
|
)
|
||||||
13
internal/server/v1/go.sum
Normal file
13
internal/server/v1/go.sum
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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/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/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/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=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
|
||||||
|
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=
|
||||||
92
internal/server/v1/handle_com.go
Normal file
92
internal/server/v1/handle_com.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package server_v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
lua "github.com/yuin/gopher-lua"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()))
|
||||||
|
|
||||||
|
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))
|
||||||
|
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))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scriptPath := filepath.Join(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))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
L := lua.NewState()
|
||||||
|
defer L.Close()
|
||||||
|
|
||||||
|
L.OpenLibs() // loads base, io, os, string, math, table, debug, package, coroutine, channel… :contentReference[oaicite:0]{index=0}
|
||||||
|
|
||||||
|
qt := h.r.URL.Query()
|
||||||
|
tbl := L.NewTable()
|
||||||
|
for k, v := range qt {
|
||||||
|
if len(v) > 0 {
|
||||||
|
L.SetField(tbl, k, lua.LString(v[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
`)
|
||||||
|
|
||||||
|
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()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[string]any)
|
||||||
|
if rt := L.GetGlobal("Result"); rt.Type() == lua.LTTable {
|
||||||
|
rt.(*lua.LTable).ForEach(func(k, v lua.LValue) {
|
||||||
|
switch v.Type() {
|
||||||
|
case lua.LTString:
|
||||||
|
out[k.String()] = v.String()
|
||||||
|
case lua.LTNumber:
|
||||||
|
out[k.String()] = float64(v.(lua.LNumber))
|
||||||
|
case lua.LTBool:
|
||||||
|
out[k.String()] = bool(v.(lua.LBool))
|
||||||
|
default:
|
||||||
|
out[k.String()] = v.String()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
case "ok":
|
||||||
|
_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))
|
||||||
|
}
|
||||||
|
_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()))
|
||||||
|
}
|
||||||
54
internal/server/v1/handle_list.go
Normal file
54
internal/server/v1/handle_list.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package server_v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
_ "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()))
|
||||||
|
type ComMeta struct {
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
files []os.DirEntry
|
||||||
|
err error
|
||||||
|
com ComMeta
|
||||||
|
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()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() || filepath.Ext(file.Name()) != ".lua" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmdName := file.Name()[:len(file.Name())-4] // remove .lua extension
|
||||||
|
if !allowedCmd.MatchString(string([]rune(cmdName)[0])) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !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()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if com.Description == "" {
|
||||||
|
com.Description = "description missing"
|
||||||
|
}
|
||||||
|
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()))
|
||||||
|
}
|
||||||
73
internal/server/v1/server.go
Normal file
73
internal/server/v1/server.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package server_v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"GoSally-mvp/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerV1Contract interface {
|
||||||
|
Handle(w http.ResponseWriter, r *http.Request)
|
||||||
|
HandleList(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
|
_handle()
|
||||||
|
_handleList()
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandlerV1 struct {
|
||||||
|
w http.ResponseWriter
|
||||||
|
r *http.Request
|
||||||
|
|
||||||
|
_log slog.Logger
|
||||||
|
|
||||||
|
cfg *config.ConfigConf
|
||||||
|
|
||||||
|
allowedCmd *regexp.Regexp
|
||||||
|
listAllowedCmd *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user