mirror of
https://github.com/akyaiy/GoSally-mvp.git
synced 2026-01-03 21:12:25 +00:00
Compare commits
146 Commits
deef4a891b
...
auth-serve
| Author | SHA1 | Date | |
|---|---|---|---|
| 625e5daf71 | |||
| cc27843bb3 | |||
| 20fec82159 | |||
| 055b299ecb | |||
| 17bf207087 | |||
| 7ae8e12dc8 | |||
| 6e36db428a | |||
| 06103a3264 | |||
| c6da55ad65 | |||
| 20a1e3e7bb | |||
| e594d519a7 | |||
| 2ceb236a53 | |||
| 811403a0a2 | |||
| b451f2d3fc | |||
| 5c01eaad6f | |||
| 2b38e179db | |||
| 2889092821 | |||
| 3df3a7b4b5 | |||
| c63f1bd123 | |||
| 095b8559f4 | |||
| 39532f22ea | |||
| 35cebee819 | |||
| 84dfdd6b35 | |||
| e693efe8e7 | |||
| c3dcf24e50 | |||
| 9e7d99e854 | |||
| 7f2783b39a | |||
| c08135309f | |||
| cd9e3ab6c4 | |||
| adaedf195f | |||
| 87694f6654 | |||
| fe628e0f7f | |||
| 3898e2833b | |||
| e4db8505a0 | |||
| 0c25d00171 | |||
| b5a6de0b62 | |||
| 1d3d74846e | |||
| 0141427bfe | |||
| 866946646b | |||
| 251e580e8a | |||
| c734779b69 | |||
| 0923f32b46 | |||
| 1c2c4c1356 | |||
| d3eb483461 | |||
| 5b32698ec5 | |||
| 0ed734b2b1 | |||
| 396352ba15 | |||
| 7b9bdcf768 | |||
| 47058f0ddd | |||
| a72627d87c | |||
| 4a9719cdfb | |||
| 7de5ec5248 | |||
| e5f9105364 | |||
| ce2a23f9e6 | |||
| d56b022bf5 | |||
| ca38c10ec4 | |||
| 13dbd00bb7 | |||
| e7289dc9be | |||
| 5394178abc | |||
| 981551e944 | |||
| 27446adf3f | |||
| 2f071c25b2 | |||
| d23fd32e84 | |||
| 86d35a9ede | |||
| c77d51a95c | |||
| 3cbea14e84 | |||
| 6e59af1662 | |||
| 8684d178e0 | |||
| 945ab6c9cf | |||
| 520901c331 | |||
| 9a274250cd | |||
| 6d49d83ea7 | |||
| fb04b3bc46 | |||
| a60b75a4c0 | |||
| 041fda8522 | |||
| 6508f03d08 | |||
| 93cf53025c | |||
| 83912b6c28 | |||
| 6ed5a7f9e0 | |||
| 2f78e9367c | |||
| ac074ce0ff | |||
| 8bdf9197d6 | |||
| 4db8fa2360 | |||
| 2a48927a08 | |||
| 58027bb988 | |||
| 30a87fdb4c | |||
| 5cdfb2a543 | |||
| 08e96aa32a | |||
| 3b8390a0c8 | |||
| b6ad0f82a0 | |||
| 7009828e79 | |||
| 45e541ac00 | |||
| a5a7354061 | |||
| 20bb90e77a | |||
| 148ca53538 | |||
| 2951fd2da9 | |||
| f411637520 | |||
| 75ee6e10aa | |||
| cfa7724b68 | |||
| f44e89b0de | |||
| 23ed707029 | |||
| 299fd59e19 | |||
| b601962354 | |||
| 38f784b850 | |||
| 6d2bf5cdd2 | |||
| 166c8470d4 | |||
| 64510a5307 | |||
| b454f4de8d | |||
| c161639766 | |||
| dd336a7d9a | |||
| ab37ecb7f7 | |||
| bd02f079ab | |||
| b97febc16e | |||
| 149cfc0a17 | |||
| 00276dc817 | |||
| ec2ef34f23 | |||
| aebc3d2e9b | |||
| 22ff90ca56 | |||
| 98d2443679 | |||
| c61bc841e6 | |||
| 74f166e6cf | |||
| 92c89996f5 | |||
| 1c73d3f87a | |||
| e35972b8ad | |||
| 0344d58ad4 | |||
| cf7bd1ceec | |||
| c3540bfbe1 | |||
| bd54628b5c | |||
| b103736a9d | |||
| 7eeedf0b31 | |||
| 1675001f24 | |||
| e01ecdf1db | |||
| febee7cac5 | |||
| bf5e136dc9 | |||
| 86cdc9adf2 | |||
| f09afdb850 | |||
| efbca43f27 | |||
| a0451aa8a0 | |||
| 7608bcfed3 | |||
| c62710a7d0 | |||
| 0151c3f68a | |||
| 1f36f2d7bc | |||
| ec94df5f4a | |||
| 19b699d92b | |||
| d5d73c1703 | |||
|
|
77baa1430e |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,6 +3,11 @@ bin/
|
||||
cert/
|
||||
tmp/
|
||||
.meta/
|
||||
.vscode
|
||||
db/
|
||||
|
||||
com/test.lua
|
||||
com/_config.lua
|
||||
|
||||
.vscode
|
||||
Taskfile.yml
|
||||
config.yaml
|
||||
19
Makefile
19
Makefile
@@ -4,7 +4,7 @@ GOPATH := $(shell go env GOPATH)
|
||||
export CONFIG_PATH := ./config.yaml
|
||||
export NODE_PATH := $(shell pwd)
|
||||
|
||||
LDFLAGS := -X 'github.com/akyaiy/GoSally-mvp/core/config.NodeVersion=v0.0.1-dev'
|
||||
LDFLAGS := -X 'github.com/akyaiy/GoSally-mvp/internal/engine/config.NodeVersion=v0.0.1-dev'
|
||||
CGO_CFLAGS := -I/usr/local/include
|
||||
CGO_LDFLAGS := -L/usr/local/lib -llua5.1 -lm -ldl
|
||||
.PHONY: all build run runq test fmt vet lint check clean
|
||||
@@ -30,7 +30,14 @@ build:
|
||||
@# @echo "CGO_CFLAGS is: '$(CGO_CFLAGS)'"
|
||||
@# @echo "CGO_LDFLAGS is: '$(CGO_LDFLAGS)'"
|
||||
@# CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)"
|
||||
go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/$(APP_NAME) ./
|
||||
@go build -trimpath -ldflags "-w -s $(LDFLAGS)" -o $(BIN_DIR)/$(APP_NAME) ./
|
||||
@if ! command -v upx >/dev/null 2>&1; then \
|
||||
echo "upx not found, skipping compression."; \
|
||||
elif upx -t $(BIN_DIR)/$(APP_NAME) >/dev/null 2>&1; then \
|
||||
echo "$(BIN_DIR)/$(APP_NAME) already compressed, skipping."; \
|
||||
else \
|
||||
upx $(BIN_DIR)/$(APP_NAME) >/dev/null 2>&1 || true; \
|
||||
fi
|
||||
|
||||
run: build
|
||||
@echo "Running!"
|
||||
@@ -48,8 +55,12 @@ test:
|
||||
@go test ./... | grep -v '^?' || true
|
||||
|
||||
fmt:
|
||||
@go fmt ./...
|
||||
@$(GOPATH)/bin/goimports -w .
|
||||
@go fmt ./internal/./...
|
||||
@go fmt ./cmd/./...
|
||||
@go fmt ./hooks/./...
|
||||
@$(GOPATH)/bin/goimports -w ./internal/
|
||||
@$(GOPATH)/bin/goimports -w ./cmd/
|
||||
@$(GOPATH)/bin/goimports -w ./hooks/
|
||||
|
||||
vet:
|
||||
@go vet ./...
|
||||
|
||||
20
cmd/root.go
20
cmd/root.go
@@ -1,3 +1,5 @@
|
||||
// The cmd package is the main package where all the main hooks and methods are called.
|
||||
// GoSally uses spf13/cobra to organize all the calls.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -5,28 +7,30 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/core/logs"
|
||||
"github.com/akyaiy/GoSally-mvp/hooks"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/colors"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var compositor *config.Compositor = config.NewCompositor()
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "node",
|
||||
Short: "Go Sally node",
|
||||
Long: "Main node runner for Go Sally",
|
||||
Long: `
|
||||
GoSally is an http server that handles jsonrpc-2.0 requests by calling methods as lua
|
||||
scripts in a given directory. For more information, visit: https://gosally.oblat.lv/`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
_ = cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
// Execute prepares global log, loads cmdline args
|
||||
// and executes rootCmd.Execute()
|
||||
func Execute() {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetPrefix(logs.SetBrightBlack(fmt.Sprintf("(%s) ", corestate.StageNotReady)))
|
||||
log.SetPrefix(colors.SetBrightBlack(fmt.Sprintf("(%s) ", corestate.StageNotReady)))
|
||||
log.SetFlags(log.Ldate | log.Ltime)
|
||||
compositor.LoadCMDLine(rootCmd)
|
||||
hooks.Compositor.LoadCMDLine(rootCmd)
|
||||
_ = rootCmd.Execute()
|
||||
// if err := rootCmd.Execute(); err != nil {
|
||||
// log.Fatalf("Unexpected error: %s", err.Error())
|
||||
|
||||
322
cmd/run.go
322
cmd/run.go
@@ -1,324 +1,18 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/app"
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
||||
gs "github.com/akyaiy/GoSally-mvp/core/general_server"
|
||||
"github.com/akyaiy/GoSally-mvp/core/logs"
|
||||
"github.com/akyaiy/GoSally-mvp/core/run_manager"
|
||||
"github.com/akyaiy/GoSally-mvp/core/sv1"
|
||||
"github.com/akyaiy/GoSally-mvp/core/update"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/akyaiy/GoSally-mvp/hooks"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/netutil"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run",
|
||||
Short: "Run node normally",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
nodeApp := app.New()
|
||||
|
||||
nodeApp.InitialHooks(
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
x.Config = compositor
|
||||
x.Log.SetOutput(os.Stdout)
|
||||
x.Log.SetPrefix(logs.SetBrightBlack(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
x.Log.SetFlags(log.Ldate | log.Ltime)
|
||||
},
|
||||
|
||||
// First stage: pre-init
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
*cs = *corestate.NewCorestate(&corestate.CoreState{
|
||||
UUID32DirName: "uuid",
|
||||
NodeBinName: filepath.Base(os.Args[0]),
|
||||
NodeVersion: config.NodeVersion,
|
||||
MetaDir: "./.meta",
|
||||
Stage: corestate.StagePreInit,
|
||||
StartTimestampUnix: time.Now().Unix(),
|
||||
})
|
||||
},
|
||||
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
x.Log.SetPrefix(logs.SetBlue(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
|
||||
if err := x.Config.LoadEnv(); err != nil {
|
||||
x.Log.Fatalf("env load error: %s", err)
|
||||
}
|
||||
cs.NodePath = x.Config.Env.NodePath
|
||||
|
||||
if cfgPath := x.Config.CMDLine.Run.ConfigPath; cfgPath != "" {
|
||||
x.Config.Env.ConfigPath = cfgPath
|
||||
}
|
||||
if err := x.Config.LoadConf(x.Config.Env.ConfigPath); err != nil {
|
||||
x.Log.Fatalf("conf load error: %s", err)
|
||||
}
|
||||
},
|
||||
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
uuid32, err := corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if err := corestate.SetNodeUUID(filepath.Join(cs.NodePath, cs.MetaDir, cs.UUID32DirName)); err != nil {
|
||||
x.Log.Fatalf("Cannod generate node uuid: %s", err.Error())
|
||||
}
|
||||
uuid32, err = corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
|
||||
if err != nil {
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
x.Log.Fatalf("uuid load error: %s", err)
|
||||
}
|
||||
cs.UUID32 = uuid32
|
||||
},
|
||||
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
if x.Config.Env.ParentStagePID != os.Getpid() {
|
||||
if os.TempDir() != "/tmp" {
|
||||
x.Log.Printf("%s: %s", logs.PrintWarn(), "Non-standard value specified for temporary directory")
|
||||
}
|
||||
// still pre-init stage
|
||||
runDir, err := run_manager.Create(cs.UUID32)
|
||||
if err != nil {
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
cs.RunDir = runDir
|
||||
input, err := os.Open(os.Args[0])
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
if err := run_manager.Set(cs.NodeBinName); err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
fmgr := run_manager.File(cs.NodeBinName)
|
||||
output, err := fmgr.Open()
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.Copy(output, input); err != nil {
|
||||
fmgr.Close()
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
if err := os.Chmod(filepath.Join(cs.RunDir, cs.NodeBinName), 0755); err != nil {
|
||||
fmgr.Close()
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
input.Close()
|
||||
fmgr.Close()
|
||||
runArgs := os.Args
|
||||
runArgs[0] = filepath.Join(cs.RunDir, cs.NodeBinName)
|
||||
|
||||
// prepare environ
|
||||
env := utils.SetEviron(os.Environ(), fmt.Sprintf("GS_PARENT_PID=%d", os.Getpid()))
|
||||
|
||||
if err := syscall.Exec(runArgs[0], runArgs, env); err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
}
|
||||
x.Log.Printf("Node uuid is %s", cs.UUID32)
|
||||
},
|
||||
|
||||
// post-init stage
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
cs.Stage = corestate.StagePostInit
|
||||
x.Log.SetPrefix(logs.SetYellow(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
|
||||
cs.RunDir = run_manager.Toggle()
|
||||
exist, err := utils.ExistsMatchingDirs(filepath.Join(os.TempDir(), fmt.Sprintf("/*-%s-%s", cs.UUID32, "gosally-runtime")), cs.RunDir)
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
if exist {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unable to continue node operation: A node with the same identifier was found in the runtime environment")
|
||||
}
|
||||
|
||||
if err := run_manager.Set("run.lock"); err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
lockPath, err := run_manager.Get("run.lock")
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
lockFile := ini.Empty()
|
||||
secRun, err := lockFile.NewSection("runtime")
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
secRun.Key("pid").SetValue(fmt.Sprintf("%d/%d", os.Getpid(), x.Config.Env.ParentStagePID))
|
||||
secRun.Key("version").SetValue(cs.NodeVersion)
|
||||
secRun.Key("uuid").SetValue(cs.UUID32)
|
||||
secRun.Key("timestamp").SetValue(time.Unix(cs.StartTimestampUnix, 0).Format("2006-01-02/15:04:05 MST"))
|
||||
secRun.Key("timestamp-unix").SetValue(fmt.Sprintf("%d", cs.StartTimestampUnix))
|
||||
|
||||
err = lockFile.SaveTo(lockPath)
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
},
|
||||
|
||||
func(cs *corestate.CoreState, x *app.AppX) {
|
||||
cs.Stage = corestate.StageReady
|
||||
x.Log.SetPrefix(logs.SetGreen(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
|
||||
x.SLog = new(slog.Logger)
|
||||
newSlog, err := logs.SetupLogger(x.Config.Conf.Log)
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
*x.SLog = *newSlog
|
||||
},
|
||||
)
|
||||
|
||||
nodeApp.Run(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
|
||||
ctxMain, cancelMain := context.WithCancel(ctx)
|
||||
runLockFile := run_manager.File("run.lock")
|
||||
_, err := runLockFile.Open()
|
||||
if err != nil {
|
||||
x.Log.Fatalf("cannot open run.lock: %s", err)
|
||||
}
|
||||
|
||||
_, err = runLockFile.Watch(ctxMain, func() {
|
||||
x.Log.Printf("run.lock was touched")
|
||||
_ = run_manager.Clean()
|
||||
cancelMain()
|
||||
})
|
||||
if err != nil {
|
||||
x.Log.Printf("watch error: %s", err)
|
||||
}
|
||||
|
||||
serverv1 := sv1.InitV1Server(&sv1.HandlerV1InitStruct{
|
||||
Log: *x.SLog,
|
||||
Config: x.Config.Conf,
|
||||
AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+$`),
|
||||
ListAllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9_-]+$`),
|
||||
Ver: "v1",
|
||||
})
|
||||
|
||||
s := gs.InitGeneral(&gs.GeneralServerInit{
|
||||
Log: x.SLog,
|
||||
Config: x.Config.Conf,
|
||||
}, serverv1)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
}))
|
||||
r.HandleFunc(config.ComDirRoute, s.Handle)
|
||||
r.Route("/favicon.ico", func(r chi.Router) {
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: x.Config.Conf.HTTPServer.Address,
|
||||
Handler: r,
|
||||
ErrorLog: log.New(&logs.SlogWriter{
|
||||
Logger: x.SLog,
|
||||
Level: logs.GlobalLevel,
|
||||
}, "", 0),
|
||||
}
|
||||
go func() {
|
||||
if x.Config.Conf.TLS.TlsEnabled {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port))
|
||||
if err != nil {
|
||||
x.Log.Printf("%s: Failed to start TLS listener: %s", logs.PrintError(), err.Error())
|
||||
return
|
||||
}
|
||||
x.Log.Printf("Serving on %s port %s with TLS... (https://%s%s)", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
|
||||
limitedListener := netutil.LimitListener(listener, 100)
|
||||
if err := srv.ServeTLS(limitedListener, x.Config.Conf.TLS.CertFile, x.Config.Conf.TLS.KeyFile); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
x.Log.Printf("%s: Failed to start HTTPS server: %s", logs.PrintError(), err.Error())
|
||||
}
|
||||
} else {
|
||||
x.Log.Printf("Serving on %s port %s... (http://%s%s)", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", x.Config.Conf.HTTPServer.Address, x.Config.Conf.HTTPServer.Port))
|
||||
if err != nil {
|
||||
x.Log.Printf("%s: Failed to start listener: %s", logs.PrintError(), err.Error())
|
||||
return
|
||||
}
|
||||
limitedListener := netutil.LimitListener(listener, 100)
|
||||
if err := srv.Serve(limitedListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
x.Log.Printf("%s: Failed to start HTTP server: %s", logs.PrintError(), err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if x.Config.Conf.Updates.UpdatesEnabled {
|
||||
go func() {
|
||||
x.Updated = update.NewUpdater(ctxMain, x.Log, x.Config.Conf, x.Config.Env)
|
||||
x.Updated.Shutdownfunc(cancelMain)
|
||||
for {
|
||||
isNewUpdate, err := x.Updated.CkeckUpdates()
|
||||
if err != nil {
|
||||
x.Log.Printf("Failed to check for updates: %s", err.Error())
|
||||
}
|
||||
if isNewUpdate {
|
||||
if err := x.Updated.Update(); err != nil {
|
||||
x.Log.Printf("Failed to update: %s", err.Error())
|
||||
} else {
|
||||
x.Log.Printf("Update completed successfully")
|
||||
}
|
||||
}
|
||||
time.Sleep(x.Config.Conf.Updates.CheckInterval)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
<-ctxMain.Done()
|
||||
if err := srv.Shutdown(ctxMain); err != nil {
|
||||
x.Log.Printf("%s: Failed to stop the server gracefully: %s", logs.PrintError(), err.Error())
|
||||
} else {
|
||||
x.Log.Printf("Server stopped gracefully")
|
||||
}
|
||||
|
||||
x.Log.Println("Cleaning up...")
|
||||
|
||||
if err := run_manager.Clean(); err != nil {
|
||||
x.Log.Printf("%s: Cleanup error: %s", logs.PrintError(), err.Error())
|
||||
}
|
||||
x.Log.Println("bye!")
|
||||
|
||||
return nil
|
||||
})
|
||||
},
|
||||
Use: "run",
|
||||
Aliases: []string{"r"},
|
||||
Short: "Run node normally",
|
||||
Long: `
|
||||
"run" starts the node with settings depending on the configuration file`,
|
||||
// hooks.Run essentially the heart of the program
|
||||
Run: hooks.Run,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
24
cmd/version.go
Normal file
24
cmd/version.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var verCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Aliases: []string{"ver", "v"},
|
||||
Short: "Return node version",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("GoSally node: %s\n", config.NodeVersion)
|
||||
fmt.Printf("Go version: %s\n", runtime.Version())
|
||||
fmt.Printf("Go OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(verCmd)
|
||||
}
|
||||
77
com/Access/GetMasterAccess.lua
Normal file
77
com/Access/GetMasterAccess.lua
Normal file
@@ -0,0 +1,77 @@
|
||||
local session = require("internal.session")
|
||||
local log = require("internal.log")
|
||||
local jwt = require("internal.crypt.jwt")
|
||||
local bc = require("internal.crypt.bcrypt")
|
||||
local db = require("internal.database.sqlite").connect("db/root.db", {log = true})
|
||||
local sha256 = require("internal.crypt.sha256")
|
||||
|
||||
log.info("Someone at "..session.request.address.." trying to get master access")
|
||||
|
||||
local function close_db()
|
||||
if db then
|
||||
db:close()
|
||||
db = nil
|
||||
end
|
||||
end
|
||||
|
||||
local params = session.request.params.get()
|
||||
|
||||
local function check_missing(arr, p)
|
||||
local is_missing = {}
|
||||
local ok = true
|
||||
for _, key in ipairs(arr) do
|
||||
if p[key] == nil then
|
||||
table.insert(is_missing, key)
|
||||
ok = false
|
||||
end
|
||||
end
|
||||
return ok, is_missing
|
||||
end
|
||||
|
||||
local ok, mp = check_missing({"master_secret", "master_name", "my_key"}, params)
|
||||
if not ok then
|
||||
close_db()
|
||||
session.response.send_error(-32602, "Missing params", mp)
|
||||
end
|
||||
|
||||
if type(params.master_secret) ~= "string" then
|
||||
close_db()
|
||||
session.response.send_error(-32050, "Access denied")
|
||||
end
|
||||
|
||||
if type(params.master_name) ~= "string" then
|
||||
close_db()
|
||||
session.response.send_error(-32050, "Access denied")
|
||||
end
|
||||
|
||||
local master, err = db:query_row("SELECT * FROM master_units WHERE master_name = ?", {params.master_name})
|
||||
|
||||
if not master then
|
||||
log.event("DB query failed:", err)
|
||||
close_db()
|
||||
session.response.send_error(-32050, "Access denied")
|
||||
end
|
||||
|
||||
local ok = bc.compare(master.master_secret, params.master_secret)
|
||||
if not ok then
|
||||
log.warn("Login failed: wrong password")
|
||||
close_db()
|
||||
session.response.send_error(-32050, "Access denied")
|
||||
end
|
||||
|
||||
local token = jwt.encode({
|
||||
secret = require("_config").token(),
|
||||
payload = {
|
||||
session_uuid = session.id,
|
||||
master_id = master.id,
|
||||
key = sha256.sum(params.my_key)
|
||||
},
|
||||
expires_in = 3600
|
||||
})
|
||||
|
||||
close_db()
|
||||
session.response.send({
|
||||
token = token
|
||||
})
|
||||
|
||||
-- G7HgOgl72o7t7u7r
|
||||
11
com/Echo.lua
Normal file
11
com/Echo.lua
Normal file
@@ -0,0 +1,11 @@
|
||||
local s = require("internal.session")
|
||||
|
||||
if not s.request.params.__fetched.data then
|
||||
s.response.error = {
|
||||
code = 123,
|
||||
message = "params.data is missing"
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
s.response.send(s.request.params.__fetched)
|
||||
57
com/List.lua
Normal file
57
com/List.lua
Normal file
@@ -0,0 +1,57 @@
|
||||
-- com/List.lua
|
||||
|
||||
local session = require("internal.session")
|
||||
|
||||
local params = session.request.params.get()
|
||||
|
||||
if params.about then
|
||||
session.response.result = {
|
||||
description = "Returns a list of available methods",
|
||||
params = {
|
||||
layer = "select which layer list to display"
|
||||
}
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
local function isValidName(name)
|
||||
return name:match("^[%w]+$") ~= nil
|
||||
end
|
||||
|
||||
local function scanDirectory(basePath, targetPath)
|
||||
local res = {}
|
||||
local fullPath = basePath.."/"..targetPath
|
||||
local handle = io.popen('find "'..fullPath..'" -type f -name "*.lua" 2>/dev/null')
|
||||
|
||||
if handle then
|
||||
for filePath in handle:lines() do
|
||||
local parts = {}
|
||||
for part in filePath:gsub(".lua$", ""):gmatch("[^/]+") do
|
||||
table.insert(parts, part)
|
||||
end
|
||||
|
||||
local allValid = true
|
||||
for _, part in ipairs(parts) do
|
||||
if not isValidName(part) then
|
||||
allValid = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if allValid then
|
||||
local relPath = filePath:gsub("^"..basePath.."/", ""):gsub(".lua$", ""):gsub("/", ">")
|
||||
table.insert(res, relPath)
|
||||
end
|
||||
end
|
||||
handle:close()
|
||||
end
|
||||
|
||||
return #res > 0 and res or nil
|
||||
end
|
||||
|
||||
local basePath = "com"
|
||||
local layer = params.layer and params.layer:gsub(">", "/") or nil
|
||||
|
||||
session.response.send({
|
||||
answer = layer and scanDirectory(basePath, layer) or scanDirectory(basePath, "")
|
||||
})
|
||||
69
com/Zones/GetZoneInfo.lua
Normal file
69
com/Zones/GetZoneInfo.lua
Normal file
@@ -0,0 +1,69 @@
|
||||
local session = require("internal.session")
|
||||
local log = require("internal.log")
|
||||
local jwt = require("internal.crypt.jwt")
|
||||
local bc = require("internal.crypt.bcrypt")
|
||||
local sha256 = require("internal.crypt.sha256")
|
||||
local dbdriver = require("internal.database.sqlite")
|
||||
|
||||
local db_root = dbdriver.connect("db/root.db", {log = true})
|
||||
local db_zone = nil
|
||||
|
||||
local function close_db()
|
||||
if db_root then
|
||||
db_root:close()
|
||||
db_root = nil
|
||||
end
|
||||
if db_zone then
|
||||
db_zone:close()
|
||||
db_zone = nil
|
||||
end
|
||||
end
|
||||
|
||||
local token = session.request.headers.get("authorization")
|
||||
|
||||
if not token or type(token) ~= "string" then
|
||||
close_db()
|
||||
session.response.send_error(-32050, "Access denied")
|
||||
end
|
||||
|
||||
local prefix = "Bearer "
|
||||
if token:sub(1, #prefix) ~= prefix then
|
||||
close_db()
|
||||
session.response.send_error(-32052, "Invalid Authorization scheme")
|
||||
end
|
||||
|
||||
local access_token = token:sub(#prefix + 1)
|
||||
|
||||
local err, data = jwt.decode(access_token, { secret = require("_config").token() })
|
||||
|
||||
if err or not data then
|
||||
close_db()
|
||||
session.response.send_error(-32053, "Cannod parse JWT", {err})
|
||||
end
|
||||
|
||||
if data.master_id then
|
||||
|
||||
|
||||
end
|
||||
|
||||
local params = session.request.params.get()
|
||||
|
||||
local function check_missing(arr, p)
|
||||
local is_missing = {}
|
||||
local ok = true
|
||||
for _, key in ipairs(arr) do
|
||||
if p[key] == nil then
|
||||
table.insert(is_missing, key)
|
||||
ok = false
|
||||
end
|
||||
end
|
||||
return ok, is_missing
|
||||
end
|
||||
|
||||
local ok, mp = check_missing({"zone_name"}, params)
|
||||
if not ok then
|
||||
close_db()
|
||||
session.response.send_error(-32602, "Missing params", mp)
|
||||
end
|
||||
|
||||
close_db()
|
||||
119
com/_Auth/DeleteUnit.lua
Normal file
119
com/_Auth/DeleteUnit.lua
Normal file
@@ -0,0 +1,119 @@
|
||||
-- com/DeleteUnit.lua
|
||||
|
||||
---@diagnostic disable: redefined-local
|
||||
local db = require("internal.database.sqlite").connect("db/user-database.db", {log = true})
|
||||
local log = require("internal.log")
|
||||
local session = require("internal.session")
|
||||
local crypt = require("internal.crypt.bcrypt")
|
||||
local jwt = require("internal.crypt.jwt")
|
||||
local sha256 = require("internal.crypt.sha256")
|
||||
|
||||
local params = session.request.params.get()
|
||||
local token = session.request.headers.get("authorization")
|
||||
|
||||
local function close_db()
|
||||
if db then
|
||||
db:close()
|
||||
db = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function error_response(message, code, data)
|
||||
session.response.error = {
|
||||
code = code or nil,
|
||||
message = message,
|
||||
data = data or nil
|
||||
}
|
||||
close_db()
|
||||
end
|
||||
|
||||
if not token or type(token) ~= "string" then
|
||||
return error_response("Access denied")
|
||||
end
|
||||
|
||||
local prefix = "Bearer "
|
||||
if token:sub(1, #prefix) ~= prefix then
|
||||
return error_response("Invalid Authorization scheme")
|
||||
end
|
||||
|
||||
local access_token = token:sub(#prefix + 1)
|
||||
|
||||
local err, data = jwt.decode(access_token, { secret = require("_config").token() })
|
||||
|
||||
if err or not data then
|
||||
session.response.error = {
|
||||
message = err
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
-- if data.session_uuid ~= session.id then
|
||||
-- return error_response("Access denied")
|
||||
-- end
|
||||
|
||||
-- if data.key ~= sha256.sum(session.request.address .. session.id .. session.request.headers.get("user-agent", "noagent")) then
|
||||
-- return error_response("Access denied")
|
||||
-- end
|
||||
|
||||
if not params then
|
||||
return error_response("no params provided")
|
||||
end
|
||||
|
||||
if not (params.username and params.email and params.password) then
|
||||
return error_response("no username/email/password provided")
|
||||
end
|
||||
|
||||
local existing, err = db:query(
|
||||
"SELECT password FROM users WHERE email = ? AND username = ? AND deleted = 0 LIMIT 1",
|
||||
{
|
||||
params.email,
|
||||
params.username
|
||||
}
|
||||
)
|
||||
|
||||
if err ~= nil then
|
||||
log.error("Password fetch failed: " .. tostring(err))
|
||||
return error_response("Database query failed: " .. tostring(err))
|
||||
end
|
||||
|
||||
if not existing or #existing == 0 then
|
||||
return error_response("Unit not found")
|
||||
end
|
||||
|
||||
local hashed_password = existing[1].password
|
||||
|
||||
local ok = crypt.compare(hashed_password, params.password)
|
||||
if not ok then
|
||||
log.warn("Wrong password attempt for: " .. params.username)
|
||||
return error_response("Invalid password")
|
||||
end
|
||||
|
||||
local ctx, err = db:exec(
|
||||
[[
|
||||
UPDATE users
|
||||
SET deleted = 1,
|
||||
deleted_at = CURRENT_TIMESTAMP
|
||||
WHERE email = ? AND username = ? AND deleted = 0
|
||||
]],
|
||||
{ params.email, params.username }
|
||||
)
|
||||
|
||||
if err ~= nil then
|
||||
log.error("Soft delete failed: " .. tostring(err))
|
||||
return error_response("Soft delete failed: " .. tostring(err))
|
||||
end
|
||||
|
||||
local res, err = ctx:wait()
|
||||
if err ~= nil then
|
||||
log.error("Soft delete confirmation failed: " .. tostring(err))
|
||||
return error_response("Soft delete confirmation failed: " .. tostring(err))
|
||||
end
|
||||
|
||||
session.response.result = {
|
||||
rows_affected = res,
|
||||
message = "Unit soft-deleted successfully"
|
||||
}
|
||||
|
||||
log.info("user " .. params.username .. " soft-deleted successfully")
|
||||
|
||||
close_db()
|
||||
76
com/_Auth/GetAccess.lua
Normal file
76
com/_Auth/GetAccess.lua
Normal file
@@ -0,0 +1,76 @@
|
||||
-- com/GetAccess
|
||||
|
||||
---@diagnostic disable: redefined-local
|
||||
local db = require("internal.database.sqlite").connect("db/user-database.db", {log = true})
|
||||
local log = require("internal.log")
|
||||
local session = require("internal.session")
|
||||
local crypt = require("internal.crypt.bcrypt")
|
||||
local jwt = require("internal.crypt.jwt")
|
||||
local sha256 = require("internal.crypt.sha256")
|
||||
|
||||
local params = session.request.params.get()
|
||||
local secret = require("_config").token()
|
||||
|
||||
local function close_db()
|
||||
if db then
|
||||
db:close()
|
||||
db = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function error_response(message, code, data)
|
||||
session.response.error = {
|
||||
code = code or nil,
|
||||
message = message,
|
||||
data = data or nil
|
||||
}
|
||||
close_db()
|
||||
end
|
||||
|
||||
if not params then
|
||||
return error_response("No params provided")
|
||||
end
|
||||
|
||||
if not (params.username and params.email and params.password) then
|
||||
return error_response("Missing username, email or password")
|
||||
end
|
||||
|
||||
local unit, err = db:query(
|
||||
"SELECT id, username, email, password, created_at FROM users WHERE email = ? AND username = ? AND deleted = 0 LIMIT 1",
|
||||
{
|
||||
params.email,
|
||||
params.username
|
||||
}
|
||||
)
|
||||
|
||||
if err then
|
||||
log.error("DB query error: " .. tostring(err))
|
||||
return error_response("Database query failed")
|
||||
end
|
||||
|
||||
if not unit or #unit == 0 then
|
||||
return error_response("Unit not found")
|
||||
end
|
||||
|
||||
unit = unit[1]
|
||||
|
||||
local ok = crypt.compare(unit.password, params.password)
|
||||
if not ok then
|
||||
log.warn("Login failed: wrong password for " .. params.username)
|
||||
return error_response("Invalid password")
|
||||
end
|
||||
|
||||
local token = jwt.encode({
|
||||
secret = secret,
|
||||
payload = { session_uuid = session.id,
|
||||
admin_user = params.username,
|
||||
key = sha256.sum(session.request.address .. session.id .. session.request.headers.get("user-agent", "noagent"))
|
||||
},
|
||||
expires_in = 3600
|
||||
})
|
||||
|
||||
session.response.result = {
|
||||
access_token = token
|
||||
}
|
||||
|
||||
close_db()
|
||||
109
com/_Auth/PutNewUnit.lua
Normal file
109
com/_Auth/PutNewUnit.lua
Normal file
@@ -0,0 +1,109 @@
|
||||
-- com/PutNewUnit.lua
|
||||
|
||||
---@diagnostic disable: redefined-local
|
||||
local db = require("internal.database.sqlite").connect("db/user-database.db", {log = true})
|
||||
local log = require("internal.log")
|
||||
local session = require("internal.session")
|
||||
local crypt = require("internal.crypt.bcrypt")
|
||||
local jwt = require("internal.crypt.jwt")
|
||||
local sha256 = require("internal.crypt.sha256")
|
||||
|
||||
local params = session.request.params.get()
|
||||
local token = session.request.headers.get("authorization")
|
||||
|
||||
local function close_db()
|
||||
if db then
|
||||
db:close()
|
||||
db = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function error_response(message, code, data)
|
||||
session.response.error = {
|
||||
code = code or nil,
|
||||
message = message,
|
||||
data = data or nil
|
||||
}
|
||||
close_db()
|
||||
end
|
||||
|
||||
if not token or type(token) ~= "string" then
|
||||
return error_response("Access denied")
|
||||
end
|
||||
|
||||
local prefix = "Bearer "
|
||||
if token:sub(1, #prefix) ~= prefix then
|
||||
return error_response("Invalid Authorization scheme")
|
||||
end
|
||||
|
||||
local access_token = token:sub(#prefix + 1)
|
||||
|
||||
local err, data = jwt.decode(access_token, { secret = require("_config").token() })
|
||||
|
||||
if err or not data then
|
||||
session.response.error = {
|
||||
message = err
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
if data.session_uuid ~= session.id then
|
||||
return error_response("Access denied")
|
||||
end
|
||||
|
||||
if data.key ~= sha256.sum(session.request.address .. session.id .. session.request.headers.get("user-agent", "noagent")) then
|
||||
return error_response("Access denied")
|
||||
end
|
||||
|
||||
if not params then
|
||||
return error_response("no params provided")
|
||||
end
|
||||
|
||||
if not (params.username and params.email and params.password) then
|
||||
return error_response("no username/email/password provided")
|
||||
end
|
||||
|
||||
local hashPass = crypt.generate(params.password, crypt.DefaultCost)
|
||||
|
||||
local existing, err = db:query("SELECT 1 FROM users WHERE deleted = 0 AND (email = ? OR username = ?) LIMIT 1", {
|
||||
params.email,
|
||||
params.username
|
||||
})
|
||||
|
||||
if err ~= nil then
|
||||
log.error("Email check failed: "..tostring(err))
|
||||
return error_response("Database check failed: "..tostring(err))
|
||||
end
|
||||
|
||||
if existing and #existing > 0 then
|
||||
return error_response("Unit already exists")
|
||||
end
|
||||
|
||||
local ctx, err = db:exec(
|
||||
"INSERT INTO users (username, email, password, first_name, last_name, phone_number) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
{
|
||||
params.username,
|
||||
params.email,
|
||||
hashPass,
|
||||
params.first_name or "",
|
||||
params.last_name or "",
|
||||
params.phone_number or ""
|
||||
}
|
||||
)
|
||||
if err ~= nil then
|
||||
log.error("Insert failed: "..tostring(err))
|
||||
return error_response("Insert failed: "..tostring(err))
|
||||
end
|
||||
|
||||
local res, err = ctx:wait()
|
||||
if err ~= nil then
|
||||
log.error("Insert confirmation failed: "..tostring(err))
|
||||
return error_response("Insert confirmation failed: "..tostring(err))
|
||||
end
|
||||
|
||||
session.response.result = {
|
||||
rows_affected = res,
|
||||
message = "Unit created successfully"
|
||||
}
|
||||
|
||||
close_db()
|
||||
66
com/_DB/Put.lua
Normal file
66
com/_DB/Put.lua
Normal file
@@ -0,0 +1,66 @@
|
||||
---@diagnostic disable: redefined-local
|
||||
local db = require("internal.database-sqlite").connect("db/test.db", {log = true})
|
||||
local log = require("internal.log")
|
||||
local session = require("internal.session")
|
||||
|
||||
if not (session.request.params.name and session.request.params.email) then
|
||||
session.response.error = {
|
||||
code = -32602,
|
||||
message = "Name and email are required"
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
local existing, err = db:query("SELECT 1 FROM users WHERE email = ? LIMIT 1", {
|
||||
session.request.params.email
|
||||
})
|
||||
if err ~= nil then
|
||||
session.response.error = {
|
||||
code = -32603,
|
||||
message = "Database check failed: "..tostring(err)
|
||||
}
|
||||
log.error("Email check failed: "..tostring(err))
|
||||
return
|
||||
end
|
||||
|
||||
if existing and #existing > 0 then
|
||||
session.response.error = {
|
||||
code = -32604,
|
||||
message = "Email already exists"
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
local ctx, err = db:exec(
|
||||
"INSERT INTO users (name, email) VALUES (?, ?)",
|
||||
{
|
||||
session.request.params.name,
|
||||
session.request.params.email
|
||||
}
|
||||
)
|
||||
if err ~= nil then
|
||||
session.response.error = {
|
||||
code = -32605,
|
||||
message = "Insert failed: "..tostring(err)
|
||||
}
|
||||
log.error("Insert failed: "..tostring(err))
|
||||
return
|
||||
end
|
||||
|
||||
local res, err = ctx:wait()
|
||||
if err ~= nil then
|
||||
session.response.error = {
|
||||
code = -32606,
|
||||
message = "Insert confirmation failed: "..tostring(err)
|
||||
}
|
||||
log.error("Insert confirmation failed: "..tostring(err))
|
||||
return
|
||||
end
|
||||
|
||||
session.response.result = {
|
||||
success = true,
|
||||
rows_affected = res,
|
||||
message = "User created successfully"
|
||||
}
|
||||
|
||||
db:close()
|
||||
29
com/_Net/GetExpl.lua
Normal file
29
com/_Net/GetExpl.lua
Normal file
@@ -0,0 +1,29 @@
|
||||
local session = require("internal.session")
|
||||
local net = require("internal.net")
|
||||
|
||||
local reqAddr
|
||||
local logReq = true
|
||||
|
||||
if session.request.params and session.request.params.url then
|
||||
reqAddr = session.request.params.url
|
||||
else
|
||||
session.response.error = {
|
||||
code = -32602,
|
||||
message = "no url provided"
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
local resp = net.http.get_request(logReq, reqAddr)
|
||||
if resp then
|
||||
session.response.result.answer = {
|
||||
status = resp.status,
|
||||
body = resp.body
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
session.response.error = {
|
||||
data = "error while requesting"
|
||||
}
|
||||
|
||||
35
com/_Net/PostExpl.lua
Normal file
35
com/_Net/PostExpl.lua
Normal file
@@ -0,0 +1,35 @@
|
||||
local session = require("internal.session")
|
||||
local net = require("internal.net")
|
||||
local log = require("internal.log")
|
||||
|
||||
local reqAddr
|
||||
local logReq = true
|
||||
local payload
|
||||
|
||||
log.debug(session.request.params)
|
||||
|
||||
if not (session.request.params and session.request.params.url) then
|
||||
session.response.error = {
|
||||
code = -32602,
|
||||
message = "no url or payload provided"
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
|
||||
reqAddr = session.request.params.url
|
||||
payload = session.request.params.payload
|
||||
|
||||
local resp = net.http.post_request(logReq, reqAddr, "application/json", payload)
|
||||
if resp then
|
||||
session.response.result.answer = {
|
||||
status = resp.status,
|
||||
body = resp.body
|
||||
}
|
||||
return
|
||||
end
|
||||
|
||||
session.response.error = {
|
||||
data = "error while requesting"
|
||||
}
|
||||
@@ -1,11 +1,54 @@
|
||||
---@alias AnyTable table<string, any>
|
||||
--@diagnostic disable: missing-fields, missing-return
|
||||
|
||||
---@type AnyTable
|
||||
In = {
|
||||
Params = {},
|
||||
}
|
||||
---@alias Any any
|
||||
---@alias AnyTable table<string, Any>
|
||||
|
||||
---@type AnyTable
|
||||
Out = {
|
||||
Result = {},
|
||||
}
|
||||
--- Global session module interface
|
||||
---@class SessionIn
|
||||
---@field params AnyTable Request parameters
|
||||
|
||||
---@class SessionOut
|
||||
---@field result Any|string? Result payload (table or primitive)
|
||||
---@field error { code: integer, message: string, data: Any }? Optional error info
|
||||
|
||||
---@class SessionModule
|
||||
---@field request SessionIn Input context (read-only)
|
||||
---@field response SessionOut Output context (write results/errors)
|
||||
|
||||
--- Global log module interface
|
||||
---@class LogModule
|
||||
---@field info fun(msg: string) Log informational message
|
||||
---@field debug fun(msg: string) Log debug message
|
||||
---@field error fun(msg: string) Log error message
|
||||
---@field warn fun(msg: string) Log warning message
|
||||
---@field event fun(msg: string) Log event (generic)
|
||||
---@field event_error fun(msg: string) Log event error
|
||||
---@field event_warn fun(msg: string) Log event warning
|
||||
|
||||
--- Global net module interface
|
||||
---@class HttpResponse
|
||||
---@field status integer HTTP status code
|
||||
---@field status_text string HTTP status text
|
||||
---@field body string Response body
|
||||
---@field content_length integer Content length
|
||||
---@field headers AnyTable Map of headers
|
||||
|
||||
---@class HttpModule
|
||||
---@field get fun(log: boolean, url: string): HttpResponse, string? Perform GET
|
||||
---@field post fun(log: boolean, url: string, content_type: string, payload: string): HttpResponse, string? Perform POST
|
||||
|
||||
---@class NetModule
|
||||
---@field http HttpModule HTTP client functions
|
||||
|
||||
--- Global variables declaration
|
||||
---@global
|
||||
---@type SessionModule
|
||||
_G.session = session or {}
|
||||
|
||||
---@global
|
||||
---@type LogModule
|
||||
_G.log = log or {}
|
||||
|
||||
---@global
|
||||
---@type NetModule
|
||||
_G.net = net or {}
|
||||
@@ -2,15 +2,3 @@
|
||||
package.path = package.path .. ";/usr/lib64/lua/5.1/?.lua;/usr/local/share/lua/5.1/?.lua" .. ";./com/?.lua;"
|
||||
package.cpath = package.cpath .. ";/usr/lib64/lua/5.1/?.so;/usr/local/lib/lua/5.1/?.so"
|
||||
|
||||
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
|
||||
|
||||
---@type table<string, any>
|
||||
Status = {
|
||||
ok = "ok",
|
||||
error = "error",
|
||||
invalid = "invalid",
|
||||
}
|
||||
|
||||
13
com/echo.lua
13
com/echo.lua
@@ -1,13 +0,0 @@
|
||||
--- #description = "Echoes back the message."
|
||||
--- #args
|
||||
--- msg = the message
|
||||
|
||||
if not In.Params.msg or In.Params.msg == "" then
|
||||
Out.Result.status = Status.error
|
||||
Out.Result.error = "Missing parameter: msg"
|
||||
return
|
||||
end
|
||||
|
||||
Out.Result.status = Status.ok
|
||||
Out.Result.answer = In.Params.msg
|
||||
return
|
||||
@@ -1,21 +1,30 @@
|
||||
mode: "prod"
|
||||
node:
|
||||
mode: dev
|
||||
name: "My gosally node"
|
||||
show_config: true
|
||||
com_dir: "%path%/com"
|
||||
|
||||
http_server:
|
||||
address: "0.0.0.0:8080"
|
||||
api:
|
||||
latest-version: v1
|
||||
layers:
|
||||
- b1
|
||||
- s2
|
||||
address: "0.0.0.0"
|
||||
port: "8080"
|
||||
session_ttl: 5s
|
||||
timeout: 3s
|
||||
idle_timeout: 30s
|
||||
|
||||
tls:
|
||||
enabled: false
|
||||
cert_file: "./cert/fullchain.pem"
|
||||
key_file: "./cert/privkey.pem"
|
||||
|
||||
com_dir: "com/"
|
||||
enabled: true
|
||||
cert_file: "%path%/cert/fullchain.pem"
|
||||
key_file: "%path%/cert/privkey.pem"
|
||||
|
||||
updates:
|
||||
enabled: true
|
||||
enabled: false
|
||||
check-interval: 1h
|
||||
repository_url: "https://repo.serve.lv/raw/go-sally"
|
||||
repository_url: "https://repo.serve.lv/raw/go-sally"
|
||||
|
||||
log:
|
||||
json_format: false
|
||||
level: "debug"
|
||||
|
||||
disable_warnings:
|
||||
- --WNonStdTmpDir
|
||||
- --WUndefLogLevel
|
||||
@@ -1,64 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/core/update"
|
||||
)
|
||||
|
||||
type AppContract interface {
|
||||
InitialHooks(fn ...func(cs *corestate.CoreState, x *AppX))
|
||||
Run(fn func(ctx context.Context, cs *corestate.CoreState, x *AppX) error)
|
||||
}
|
||||
|
||||
type App struct {
|
||||
initHooks []func(cs *corestate.CoreState, x *AppX)
|
||||
runHook func(ctx context.Context, cs *corestate.CoreState, x *AppX) error
|
||||
|
||||
Corestate *corestate.CoreState
|
||||
AppX *AppX
|
||||
}
|
||||
|
||||
type AppX struct {
|
||||
Config *config.Compositor
|
||||
Log *log.Logger
|
||||
SLog *slog.Logger
|
||||
Updated *update.Updater
|
||||
}
|
||||
|
||||
func New() AppContract {
|
||||
return &App{
|
||||
AppX: &AppX{
|
||||
Log: log.Default(),
|
||||
},
|
||||
Corestate: &corestate.CoreState{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) InitialHooks(fn ...func(cs *corestate.CoreState, x *AppX)) {
|
||||
a.initHooks = append(a.initHooks, fn...)
|
||||
}
|
||||
|
||||
func (a *App) Run(fn func(ctx context.Context, cs *corestate.CoreState, x *AppX) error) {
|
||||
a.runHook = fn
|
||||
|
||||
for _, hook := range a.initHooks {
|
||||
hook(a.Corestate, a.AppX)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
defer stop()
|
||||
|
||||
if a.runHook != nil {
|
||||
if err := a.runHook(ctx, a.Corestate, a.AppX); err != nil {
|
||||
log.Fatalf("fatal in Run: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Package config provides configuration management for the application.
|
||||
// config is built on top of the third-party module cleanenv
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type CompositorContract interface {
|
||||
LoadEnv() error
|
||||
LoadConf(path string) error
|
||||
}
|
||||
|
||||
type Compositor struct {
|
||||
CMDLine *CMDLine
|
||||
Conf *Conf
|
||||
Env *Env
|
||||
}
|
||||
|
||||
type Conf struct {
|
||||
Mode string `mapstructure:"mode"`
|
||||
ComDir string `mapstructure:"com_dir"`
|
||||
HTTPServer HTTPServer `mapstructure:"http_server"`
|
||||
TLS TLS `mapstructure:"tls"`
|
||||
Updates Updates `mapstructure:"updates"`
|
||||
Log Log `mapstructure:"log"`
|
||||
}
|
||||
|
||||
type HTTPServer struct {
|
||||
Address string `mapstructure:"address"`
|
||||
Port string `mapstructure:"port"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
IdleTimeout time.Duration `mapstructure:"idle_timeout"`
|
||||
HTTPServer_Api HTTPServer_Api `mapstructure:"api"`
|
||||
}
|
||||
|
||||
type HTTPServer_Api struct {
|
||||
LatestVer string `mapstructure:"latest-version"`
|
||||
Layers []string `mapstructure:"layers"`
|
||||
}
|
||||
|
||||
type TLS struct {
|
||||
TlsEnabled bool `mapstructure:"enabled"`
|
||||
CertFile string `mapstructure:"cert_file"`
|
||||
KeyFile string `mapstructure:"key_file"`
|
||||
}
|
||||
|
||||
type Updates struct {
|
||||
UpdatesEnabled bool `mapstructure:"enabled"`
|
||||
CheckInterval time.Duration `mapstructure:"check_interval"`
|
||||
RepositoryURL string `mapstructure:"repository_url"`
|
||||
WantedVersion string `mapstructure:"wanted_version"`
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
Level string `mapstructure:"level"`
|
||||
OutPath string `mapstructure:"out_path"`
|
||||
}
|
||||
|
||||
// ConfigEnv structure for environment variables
|
||||
type Env struct {
|
||||
ConfigPath string `mapstructure:"config_path"`
|
||||
NodePath string `mapstructure:"node_path"`
|
||||
ParentStagePID int `mapstructure:"parent_pid"`
|
||||
}
|
||||
|
||||
type CMDLine struct {
|
||||
Run Run
|
||||
Node Root
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
Debug bool `persistent:"true" full:"debug" short:"d" def:"false" desc:"Set debug mode"`
|
||||
}
|
||||
|
||||
type Run struct {
|
||||
ConfigPath string `persistent:"true" full:"config" short:"c" def:"./config.yaml" desc:"Path to configuration file"`
|
||||
Test []int `persistent:"true" full:"test" short:"t" def:"" desc:"js test"`
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
// Package general_server provides an API request router based on versioning and custom layers.
|
||||
//
|
||||
// The GeneralServer distributes incoming HTTP requests to specific registered servers
|
||||
// depending on the API version or defined logical layer. To operate properly, additional
|
||||
// servers must be registered using the InitGeneral function or AppendToArray method.
|
||||
//
|
||||
// All registered servers must implement the GeneralServerApiContract interface to ensure
|
||||
// correct interaction. The GeneralServer itself implements this interface and can be
|
||||
// passed as an HTTP handler.
|
||||
//
|
||||
// If the requested version is not explicitly registered but matches a configured logical
|
||||
// layer, the server will fallback to the latest registered version for that layer.
|
||||
// Otherwise, an HTTP 400 error is returned.
|
||||
package general_server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// serversApiVer is a type alias for string, used to represent API version strings in the GeneralServer.
|
||||
type serversApiVer string
|
||||
|
||||
// GeneralServerApiContract defines the interface for servers that can be registered
|
||||
type GeneralServerApiContract interface {
|
||||
// GetVersion returns the API version of the server.
|
||||
GetVersion() string
|
||||
|
||||
// Handle and HandleList methods are used to forward requests.
|
||||
Handle(w http.ResponseWriter, r *http.Request)
|
||||
HandleList(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// GeneralServerContarct extends the GeneralServerApiContract with a method to append new servers.
|
||||
// This interface is only for general server initialization and does not need to be implemented by individual servers.
|
||||
type GeneralServerContarct interface {
|
||||
GeneralServerApiContract
|
||||
// AppendToArray adds a new server to the GeneralServer's internal map.
|
||||
AppendToArray(GeneralServerApiContract) error
|
||||
}
|
||||
|
||||
// GeneralServer implements the GeneralServerApiContract and serves as a router for different API versions.
|
||||
type GeneralServer struct {
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
|
||||
// servers holds the registered servers by their API version.
|
||||
// The key is the version string, and the value is the server implementing GeneralServerApi
|
||||
servers map[serversApiVer]GeneralServerApiContract
|
||||
|
||||
log slog.Logger
|
||||
cfg *config.Conf
|
||||
}
|
||||
|
||||
// GeneralServerInit structure only for initialization general server.
|
||||
type GeneralServerInit struct {
|
||||
Log slog.Logger
|
||||
Config *config.Conf
|
||||
}
|
||||
|
||||
// InitGeneral initializes a new GeneralServer with the provided configuration and registered servers.
|
||||
func InitGeneral(o *GeneralServerInit, servers ...GeneralServerApiContract) *GeneralServer {
|
||||
general := &GeneralServer{
|
||||
servers: make(map[serversApiVer]GeneralServerApiContract),
|
||||
cfg: o.Config,
|
||||
log: o.Log,
|
||||
}
|
||||
|
||||
// register the provided servers
|
||||
// s is each server implementing GeneralServerApiContract, this is not a general server
|
||||
for _, s := range servers {
|
||||
general.servers[serversApiVer(s.GetVersion())] = s
|
||||
}
|
||||
return general
|
||||
}
|
||||
|
||||
// GetVersion returns the API version of the GeneralServer, which is "general".
|
||||
func (s *GeneralServer) GetVersion() string {
|
||||
return "general"
|
||||
}
|
||||
|
||||
// AppendToArray adds a new server to the GeneralServer's internal map.
|
||||
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")
|
||||
}
|
||||
|
||||
// Handle processes incoming HTTP requests, routing them to the appropriate server based on the API version.
|
||||
// It checks if the requested version is registered and handles the request accordingly.
|
||||
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")
|
||||
|
||||
// transfer control to the server
|
||||
if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok {
|
||||
srv.Handle(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// if the requested version is not registered, check if it matches a logical layer
|
||||
// and use the latest version for that layer if available
|
||||
// this allows for custom layers to be defined in the configuration
|
||||
// and used as a fallback for unsupported versions
|
||||
// this is useful for cases where the API version is not explicitly registered
|
||||
// but the logical layer is defined in the configuration
|
||||
if slices.Contains(s.cfg.HTTPServer.HTTPServer_Api.Layers, serverReqApiVer) {
|
||||
if srv, ok := s.servers[serversApiVer(s.cfg.HTTPServer.HTTPServer_Api.LatestVer)]; ok {
|
||||
s.log.Debug("Using latest version under custom layer",
|
||||
slog.String("layer", serverReqApiVer),
|
||||
slog.String("fallback-version", s.cfg.HTTPServer.HTTPServer_Api.LatestVer),
|
||||
)
|
||||
// transfer control to the latest version server under the custom layer
|
||||
srv.Handle(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("HTTP request error: unsupported API version",
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
if err := utils.WriteJSONError(s.w, http.StatusBadRequest, "unsupported API version"); err != nil {
|
||||
s.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
// HandleList processes incoming HTTP requests for listing commands, routing them to the appropriate server based on the 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")
|
||||
|
||||
// transfer control to the server
|
||||
if srv, ok := s.servers[serversApiVer(serverReqApiVer)]; ok {
|
||||
srv.HandleList(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(s.cfg.HTTPServer.HTTPServer_Api.Layers, serverReqApiVer) {
|
||||
if srv, ok := s.servers[serversApiVer(s.cfg.HTTPServer.HTTPServer_Api.LatestVer)]; ok {
|
||||
log.Debug("Using latest version under custom layer",
|
||||
slog.String("layer", serverReqApiVer),
|
||||
slog.String("fallback-version", s.cfg.HTTPServer.HTTPServer_Api.LatestVer),
|
||||
)
|
||||
// transfer control to the latest version server under the custom layer
|
||||
srv.HandleList(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("HTTP request error: unsupported API version",
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
if err := utils.WriteJSONError(s.w, http.StatusBadRequest, "unsupported API version"); err != nil {
|
||||
s.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// HandlerV1 is the main handler for version 1 of the API.
|
||||
// The function processes the HTTP request and runs Lua scripts,
|
||||
// preparing the environment and subsequently transmitting the execution result
|
||||
func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
uuid16, err := utils.NewUUID(int(config.UUIDLength))
|
||||
if err != nil {
|
||||
h.log.Error("Failed to generate UUID",
|
||||
slog.String("error", err.Error()))
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error()); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
log := h.log.With(
|
||||
slog.Group("request",
|
||||
slog.String("version", h.GetVersion()),
|
||||
slog.String("url", r.URL.String()),
|
||||
slog.String("method", r.Method),
|
||||
),
|
||||
slog.Group("connection",
|
||||
slog.String("connection-uuid", uuid16),
|
||||
slog.String("remote", r.RemoteAddr),
|
||||
),
|
||||
)
|
||||
log.Info("Received request")
|
||||
|
||||
cmd := chi.URLParam(r, "cmd")
|
||||
if !h.allowedCmd.MatchString(string([]rune(cmd)[0])) || !h.listAllowedCmd.MatchString(cmd) {
|
||||
log.Error("HTTP request error",
|
||||
slog.String("error", "invalid command"),
|
||||
slog.String("cmd", cmd),
|
||||
slog.Int("status", http.StatusBadRequest))
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusBadRequest, "invalid command"); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
scriptPath := h.comMatch(chi.URLParam(r, "ver"), cmd)
|
||||
if scriptPath == "" {
|
||||
log.Error("HTTP request error",
|
||||
slog.String("error", "command not found"),
|
||||
slog.String("cmd", cmd),
|
||||
slog.Int("status", http.StatusNotFound))
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusNotFound, "command not found"); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
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))
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusNotFound, "command not found"); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
paramsTable := L.NewTable()
|
||||
qt := r.URL.Query()
|
||||
for k, v := range qt {
|
||||
if len(v) > 0 {
|
||||
L.SetField(paramsTable, k, lua.LString(v[0]))
|
||||
}
|
||||
}
|
||||
inTable := L.NewTable()
|
||||
L.SetField(inTable, "Params", paramsTable)
|
||||
L.SetGlobal("In", inTable)
|
||||
|
||||
// Создаем таблицу Out с Result
|
||||
resultTable := L.NewTable()
|
||||
outTable := L.NewTable()
|
||||
L.SetField(outTable, "Result", resultTable)
|
||||
L.SetGlobal("Out", outTable)
|
||||
|
||||
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()))
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "lua error: "+err.Error()); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.Warn("No environment preparation script found, skipping preparation")
|
||||
}
|
||||
|
||||
if err := L.DoFile(scriptPath); err != nil {
|
||||
log.Error("Failed to execute lua script",
|
||||
slog.String("error", err.Error()))
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "lua error: "+err.Error()); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
lv := L.GetGlobal("Out")
|
||||
tbl, ok := lv.(*lua.LTable)
|
||||
if !ok {
|
||||
log.Error("Lua global 'Out' is not a table")
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "'Out' is not a table"); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resultVal := tbl.RawGetString("Result")
|
||||
resultTbl, ok := resultVal.(*lua.LTable)
|
||||
if !ok {
|
||||
log.Error("Lua global 'Result' is not a table")
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "'Result' is not a table"); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
out := make(map[string]any)
|
||||
resultTbl.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
out[key.String()] = utils.ConvertLuaTypesToGolang(value)
|
||||
})
|
||||
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.MetaDir, "uuid"))
|
||||
response := ResponseFormat{
|
||||
ResponsibleAgentUUID: uuid32,
|
||||
RequestedCommand: cmd,
|
||||
Response: out,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
log.Error("Failed to encode JSON response",
|
||||
slog.String("error", err.Error()))
|
||||
}
|
||||
|
||||
status, _ := out["status"].(string)
|
||||
switch 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))
|
||||
}
|
||||
|
||||
log.Info("Session completed")
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// The function processes the HTTP request and returns a list of available commands.
|
||||
func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||
uuid16, err := utils.NewUUID(int(config.UUIDLength))
|
||||
if err != nil {
|
||||
h.log.Error("Failed to generate UUID",
|
||||
slog.String("error", err.Error()))
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error()); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
log := h.log.With(
|
||||
slog.Group("request",
|
||||
slog.String("version", h.GetVersion()),
|
||||
slog.String("url", r.URL.String()),
|
||||
slog.String("method", r.Method),
|
||||
),
|
||||
slog.Group("connection",
|
||||
slog.String("connection-uuid", uuid16),
|
||||
slog.String("remote", r.RemoteAddr),
|
||||
),
|
||||
)
|
||||
log.Info("Received request")
|
||||
type ComMeta struct {
|
||||
Description string `json:"Description"`
|
||||
Arguments map[string]string `json:"Arguments,omitempty"`
|
||||
}
|
||||
var (
|
||||
files []os.DirEntry
|
||||
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()))
|
||||
|
||||
if err := utils.WriteJSONError(w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error()); err != nil {
|
||||
h.log.Error("Failed to write JSON", slog.String("err", err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
apiVer := chi.URLParam(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")
|
||||
uuid32, _ := corestate.GetNodeUUID(filepath.Join(config.MetaDir, "uuid"))
|
||||
response := ResponseFormat{
|
||||
ResponsibleAgentUUID: uuid32,
|
||||
RequestedCommand: "list",
|
||||
Response: commands,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
h.log.Error("Failed to write JSON error response",
|
||||
slog.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package sv1
|
||||
|
||||
type ResponseFormat struct {
|
||||
ResponsibleAgentUUID string
|
||||
RequestedCommand string
|
||||
Response any
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// func (h *HandlerV1) errNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
// utils.WriteJSONError(h.w, 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) 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
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package utils
|
||||
|
||||
import lua "github.com/yuin/gopher-lua"
|
||||
|
||||
func ConvertLuaTypesToGolang(value lua.LValue) any {
|
||||
switch value.Type() {
|
||||
case lua.LTString:
|
||||
return value.String()
|
||||
case lua.LTNumber:
|
||||
return float64(value.(lua.LNumber))
|
||||
case lua.LTBool:
|
||||
return bool(value.(lua.LBool))
|
||||
case lua.LTTable:
|
||||
result := make(map[string]interface{})
|
||||
if tbl, ok := value.(*lua.LTable); ok {
|
||||
tbl.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
result[key.String()] = ConvertLuaTypesToGolang(value)
|
||||
})
|
||||
}
|
||||
return result
|
||||
case lua.LTNil:
|
||||
return nil
|
||||
default:
|
||||
return value.String()
|
||||
}
|
||||
}
|
||||
47
go.mod
47
go.mod
@@ -4,35 +4,42 @@ go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/yuin/gopher-lua v1.1.1
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/crypto v0.40.0
|
||||
golang.org/x/net v0.42.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
modernc.org/sqlite v1.38.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/viper v1.20.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sagikazarmark/locafero v0.10.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.9.2 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
modernc.org/libc v1.66.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/go-chi/cors v1.2.2
|
||||
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
|
||||
)
|
||||
|
||||
131
go.sum
131
go.sum
@@ -1,62 +1,117 @@
|
||||
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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
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/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
|
||||
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc=
|
||||
github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
|
||||
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
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=
|
||||
modernc.org/cc/v4 v4.26.3 h1:yEN8dzrkRFnn4PUUKXLYIqVf2PJYAEjMTFjO3BDGc3I=
|
||||
modernc.org/cc/v4 v4.26.3/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.6 h1:RyQpwAhM/19nXD8y3iejM/AjmKwY2TjxZTlUWTsWw2U=
|
||||
modernc.org/libc v1.66.6/go.mod h1:j8z0EYAuumoMQ3+cWXtmw6m+LYn3qm8dcZDFtFTSq+M=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
416
hooks/initial.go
Normal file
416
hooks/initial.go
Normal file
@@ -0,0 +1,416 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/colors"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/run_manager"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/logs"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// The config composer needs to be in the global scope
|
||||
var Compositor *config.Compositor = config.NewCompositor()
|
||||
|
||||
func InitGlobalLoggerHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
x.Config = Compositor
|
||||
x.Log.SetOutput(os.Stdout)
|
||||
x.Log.SetPrefix(colors.SetBrightBlack(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
x.Log.SetFlags(log.Ldate | log.Ltime)
|
||||
}
|
||||
|
||||
// First stage: pre-init
|
||||
func InitCorestateHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
*cs = *corestate.NewCorestate(&corestate.CoreState{
|
||||
UUID32DirName: "uuid",
|
||||
NodeBinName: filepath.Base(os.Args[0]),
|
||||
NodeVersion: config.NodeVersion,
|
||||
MetaDir: "./.meta",
|
||||
Stage: corestate.StagePreInit,
|
||||
StartTimestampUnix: time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
func InitConfigLoadHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
x.Log.SetPrefix(colors.SetYellow(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
|
||||
if err := x.Config.LoadEnv(); err != nil {
|
||||
x.Log.Fatalf("env load error: %s", err)
|
||||
}
|
||||
cs.NodePath = *x.Config.Env.NodePath
|
||||
|
||||
if cfgPath := x.Config.CMDLine.Run.ConfigPath; cfgPath != "" {
|
||||
x.Config.Env.ConfigPath = &cfgPath
|
||||
}
|
||||
if err := x.Config.LoadConf(*x.Config.Env.ConfigPath); err != nil {
|
||||
x.Log.Fatalf("conf load error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The hook reads or prepares a persistent uuid for the node
|
||||
func InitUUIDHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
uuid32, err := corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if err := corestate.SetNodeUUID(filepath.Join(cs.NodePath, cs.MetaDir, cs.UUID32DirName)); err != nil {
|
||||
x.Log.Fatalf("Cannod generate node uuid: %s", err.Error())
|
||||
}
|
||||
uuid32, err = corestate.GetNodeUUID(filepath.Join(cs.MetaDir, "uuid"))
|
||||
if err != nil {
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
x.Log.Fatalf("uuid load error: %s", err)
|
||||
}
|
||||
cs.UUID32 = uuid32
|
||||
corestate.NODE_UUID = uuid32
|
||||
}
|
||||
|
||||
// The hook is responsible for checking the initialization stage
|
||||
// and restarting in some cases
|
||||
func InitRuntimeHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
if *x.Config.Env.ParentStagePID != os.Getpid() {
|
||||
// still pre-init stage
|
||||
runDir, err := run_manager.Create(cs.UUID32)
|
||||
if err != nil {
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
cs.RunDir = runDir
|
||||
input, err := os.Open(os.Args[0])
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
if err := run_manager.Set(cs.NodeBinName); err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
fmgr := run_manager.File(cs.NodeBinName)
|
||||
output, err := fmgr.Open()
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.Copy(output, input); err != nil {
|
||||
fmgr.Close()
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
if err := os.Chmod(filepath.Join(cs.RunDir, cs.NodeBinName), 0755); err != nil {
|
||||
fmgr.Close()
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
input.Close()
|
||||
fmgr.Close()
|
||||
runArgs := os.Args
|
||||
runArgs[0] = filepath.Join(cs.RunDir, cs.NodeBinName)
|
||||
|
||||
// prepare environ
|
||||
env := utils.SetEviron(os.Environ(), fmt.Sprintf("GS_PARENT_PID=%d", os.Getpid()))
|
||||
|
||||
if err := syscall.Exec(runArgs[0], runArgs, env); err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
}
|
||||
x.Log.Printf("Node uuid is %s", cs.UUID32)
|
||||
}
|
||||
|
||||
// post-init stage
|
||||
// The hook creates a run.lock file, which contains information
|
||||
// about the process and the node, in the runtime directory.
|
||||
func InitRunlockHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
NodeApp.Fallback(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
x.Log.Println("Cleaning up...")
|
||||
|
||||
if err := run_manager.Clean(); err != nil {
|
||||
x.Log.Printf("%s: Cleanup error: %s", colors.PrintError(), err.Error())
|
||||
}
|
||||
x.Log.Println("bye!")
|
||||
})
|
||||
|
||||
cs.Stage = corestate.StagePostInit
|
||||
x.Log.SetPrefix(colors.SetBlue(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
|
||||
cs.RunDir = run_manager.Toggle()
|
||||
exist, err := utils.ExistsMatchingDirs(filepath.Join(os.TempDir(), fmt.Sprintf("/*-%s-%s", cs.UUID32, "gosally-runtime")), cs.RunDir)
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
if exist {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unable to continue node operation: A node with the same identifier was found in the runtime environment")
|
||||
}
|
||||
|
||||
if err := run_manager.Set("run.lock"); err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
lockPath, err := run_manager.Get("run.lock")
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
lockFile := ini.Empty()
|
||||
secRun, err := lockFile.NewSection("runtime")
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
secRun.Key("pid").SetValue(fmt.Sprintf("%d/%d", os.Getpid(), x.Config.Env.ParentStagePID))
|
||||
secRun.Key("version").SetValue(cs.NodeVersion)
|
||||
secRun.Key("uuid").SetValue(cs.UUID32)
|
||||
secRun.Key("timestamp").SetValue(time.Unix(cs.StartTimestampUnix, 0).Format("2006-01-02/15:04:05 MST"))
|
||||
secRun.Key("timestamp-unix").SetValue(fmt.Sprintf("%d", cs.StartTimestampUnix))
|
||||
|
||||
err = lockFile.SaveTo(lockPath)
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// The hook reads the configuration and replaces special expressions
|
||||
// (%tmp% and so on) in string fields with the required data.
|
||||
func InitConfigReplHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
if !slices.Contains(*x.Config.Conf.DisableWarnings, "--WNonStdTmpDir") && os.TempDir() != "/tmp" {
|
||||
x.Log.Printf("%s: %s", colors.PrintWarn(), "Non-standard value specified for temporary directory")
|
||||
}
|
||||
|
||||
replacements := map[string]any{
|
||||
"%tmp%": filepath.Clean(run_manager.RuntimeDir()),
|
||||
"%path%": *x.Config.Env.NodePath,
|
||||
"%stdout%": "_1STDout",
|
||||
"%stderr%": "_2STDerr",
|
||||
"%1%": "_1STDout",
|
||||
"%2%": "_2STDerr",
|
||||
}
|
||||
|
||||
processConfig(&x.Config.Conf, replacements)
|
||||
|
||||
if !slices.Contains(logs.Levels.Available, *x.Config.Conf.Log.Level) {
|
||||
if !slices.Contains(*x.Config.Conf.DisableWarnings, "--WUndefLogLevel") {
|
||||
x.Log.Printf("%s: %s", colors.PrintWarn(), fmt.Sprintf("Unknown logging level %s, fallback level: %s", *x.Config.Conf.Log.Level, logs.Levels.Fallback))
|
||||
}
|
||||
x.Config.Conf.Log.Level = &logs.Levels.Fallback
|
||||
}
|
||||
}
|
||||
|
||||
// The hook is responsible for outputting the
|
||||
// final config and asking for confirmation.
|
||||
func InitConfigPrintHook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
if *x.Config.Conf.Node.ShowConfig {
|
||||
fmt.Printf("Configuration from %s:\n", x.Config.CMDLine.Run.ConfigPath)
|
||||
x.Config.Print(x.Config.Conf)
|
||||
|
||||
fmt.Printf("Environment:\n")
|
||||
x.Config.Print(x.Config.Env)
|
||||
|
||||
if cs.UUID32 != "" && !askConfirm("Is that ok?", true) {
|
||||
x.Log.Printf("Cancel launch")
|
||||
NodeApp.CallFallback(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
x.Log.Printf("Starting \"%s\" node", *x.Config.Conf.Node.Name)
|
||||
}
|
||||
|
||||
func InitSLogHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
cs.Stage = corestate.StageReady
|
||||
x.Log.SetPrefix(colors.SetGreen(fmt.Sprintf("(%s) ", cs.Stage)))
|
||||
|
||||
x.SLog = new(slog.Logger)
|
||||
newSlog, err := logs.SetupLogger(x.Config.Conf.Log)
|
||||
if err != nil {
|
||||
_ = run_manager.Clean()
|
||||
x.Log.Fatalf("Unexpected failure: %s", err.Error())
|
||||
}
|
||||
*x.SLog = *newSlog
|
||||
}
|
||||
|
||||
// The method goes through the entire config structure through
|
||||
// reflection and replaces string fields with the required ones.
|
||||
func processConfig(conf any, replacements map[string]any) error {
|
||||
val := reflect.ValueOf(conf)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Struct:
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
if field.CanAddr() && field.CanSet() {
|
||||
if err := processConfig(field.Addr().Interface(), replacements); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
elem := val.Index(i)
|
||||
if elem.CanAddr() && elem.CanSet() {
|
||||
if err := processConfig(elem.Addr().Interface(), replacements); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Map:
|
||||
for _, key := range val.MapKeys() {
|
||||
elem := val.MapIndex(key)
|
||||
if elem.CanInterface() {
|
||||
newVal := reflect.New(elem.Type()).Elem()
|
||||
newVal.Set(elem)
|
||||
|
||||
if err := processConfig(newVal.Addr().Interface(), replacements); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val.SetMapIndex(key, newVal)
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.String:
|
||||
str := val.String()
|
||||
|
||||
if replacement, exists := replacements[str]; exists {
|
||||
if err := setValue(val, replacement); err != nil {
|
||||
return fmt.Errorf("failed to set %q: %v", str, err)
|
||||
}
|
||||
} else {
|
||||
for placeholder, replacement := range replacements {
|
||||
if strings.Contains(str, placeholder) {
|
||||
replacementStr, err := toString(replacement)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid replacement for %q: %v", placeholder, err)
|
||||
}
|
||||
newStr := strings.ReplaceAll(str, placeholder, replacementStr)
|
||||
val.SetString(newStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Ptr:
|
||||
if !val.IsNil() {
|
||||
elem := val.Elem()
|
||||
if elem.Kind() == reflect.String {
|
||||
str := elem.String()
|
||||
if replacement, exists := replacements[str]; exists {
|
||||
strVal, err := toString(replacement)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot convert replacement to string: %v", err)
|
||||
}
|
||||
elem.SetString(strVal)
|
||||
} else {
|
||||
for placeholder, replacement := range replacements {
|
||||
if strings.Contains(str, placeholder) {
|
||||
replacementStr, err := toString(replacement)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid replacement for %q: %v", placeholder, err)
|
||||
}
|
||||
newStr := strings.ReplaceAll(str, placeholder, replacementStr)
|
||||
elem.SetString(newStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return processConfig(elem.Addr().Interface(), replacements)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setValue(val reflect.Value, replacement any) error {
|
||||
if !val.CanSet() {
|
||||
return fmt.Errorf("value is not settable")
|
||||
}
|
||||
|
||||
replacementVal := reflect.ValueOf(replacement)
|
||||
if replacementVal.Type().AssignableTo(val.Type()) {
|
||||
val.Set(replacementVal)
|
||||
return nil
|
||||
}
|
||||
|
||||
if val.Kind() == reflect.String {
|
||||
str, err := toString(replacement)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot convert replacement to string: %v", err)
|
||||
}
|
||||
val.SetString(str)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("type mismatch: cannot assign %T to %v", replacement, val.Type())
|
||||
}
|
||||
|
||||
func toString(v any) (string, error) {
|
||||
switch s := v.(type) {
|
||||
case string:
|
||||
return s, nil
|
||||
case fmt.Stringer:
|
||||
return s.String(), nil
|
||||
default:
|
||||
return fmt.Sprint(v), nil
|
||||
}
|
||||
}
|
||||
|
||||
func askConfirm(prompt string, defaultYes bool) bool {
|
||||
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
|
||||
fmt.Print(prompt)
|
||||
if defaultYes {
|
||||
fmt.Printf(" (%s/%s): ", colors.SetBrightGreen("Y"), colors.SetBrightRed("n"))
|
||||
} else {
|
||||
fmt.Printf(" (%s/%s): ", colors.SetBrightGreen("n"), colors.SetBrightRed("Y"))
|
||||
}
|
||||
|
||||
inputChan := make(chan string, 1)
|
||||
|
||||
go func() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
text, _ := reader.ReadString('\n')
|
||||
inputChan <- text
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
fmt.Println("")
|
||||
NodeApp.CallFallback(ctx)
|
||||
os.Exit(3)
|
||||
case text := <-inputChan:
|
||||
text = strings.TrimSpace(strings.ToLower(text))
|
||||
if text == "" {
|
||||
return defaultYes
|
||||
}
|
||||
if text == "y" || text == "yes" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return defaultYes
|
||||
}
|
||||
176
hooks/run.go
Normal file
176
hooks/run.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/colors"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/run_manager"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/update"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/logs"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/gateway"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/session"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/sv1"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/netutil"
|
||||
)
|
||||
|
||||
var NodeApp = app.New()
|
||||
|
||||
func Run(cmd *cobra.Command, args []string) {
|
||||
NodeApp.InitialHooks(
|
||||
InitGlobalLoggerHook, InitCorestateHook, InitConfigLoadHook,
|
||||
InitUUIDHook, InitRuntimeHook, InitRunlockHook,
|
||||
InitConfigReplHook, InitConfigPrintHook, InitSLogHook,
|
||||
)
|
||||
|
||||
NodeApp.Run(RunHook)
|
||||
}
|
||||
|
||||
func RunHook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
|
||||
ctxMain, cancelMain := context.WithCancel(ctx)
|
||||
runLockFile := run_manager.File("run.lock")
|
||||
_, err := runLockFile.Open()
|
||||
if err != nil {
|
||||
x.Log.Fatalf("cannot open run.lock: %s", err)
|
||||
}
|
||||
|
||||
_, err = runLockFile.Watch(ctxMain, func() {
|
||||
x.Log.Printf("run.lock was touched")
|
||||
_ = run_manager.Clean()
|
||||
cancelMain()
|
||||
})
|
||||
if err != nil {
|
||||
x.Log.Printf("watch error: %s", err)
|
||||
}
|
||||
|
||||
serverv1 := sv1.InitV1Server(&sv1.HandlerV1InitStruct{
|
||||
X: x,
|
||||
CS: cs,
|
||||
AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+(>[a-zA-Z0-9]+)*$`),
|
||||
Ver: "v1",
|
||||
})
|
||||
|
||||
session_manager := session.New(*x.Config.Conf.HTTPServer.SessionTTL)
|
||||
|
||||
s := gateway.InitGateway(&gateway.GatewayServerInit{
|
||||
SM: session_manager,
|
||||
CS: cs,
|
||||
X: x,
|
||||
}, serverv1)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
AllowedMethods: []string{"POST"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-Session-UUID"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300,
|
||||
}))
|
||||
r.HandleFunc(config.ComDirRoute, s.Handle)
|
||||
r.Route("/favicon.ico", func(r chi.Router) {
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: *x.Config.Conf.HTTPServer.Address,
|
||||
Handler: r,
|
||||
ErrorLog: log.New(&logs.SlogWriter{
|
||||
Logger: x.SLog,
|
||||
Level: slog.LevelError,
|
||||
}, "", 0),
|
||||
}
|
||||
|
||||
NodeApp.Fallback(func(ctx context.Context, cs *corestate.CoreState, x *app.AppX) {
|
||||
if err := srv.Shutdown(ctxMain); err != nil {
|
||||
x.Log.Printf("%s: Failed to stop the server gracefully: %s", colors.PrintError(), err.Error())
|
||||
} else {
|
||||
x.Log.Printf("Server stopped gracefully")
|
||||
}
|
||||
|
||||
x.Log.Println("Cleaning up...")
|
||||
|
||||
if err := run_manager.Clean(); err != nil {
|
||||
x.Log.Printf("%s: Cleanup error: %s", colors.PrintError(), err.Error())
|
||||
}
|
||||
x.Log.Println("bye!")
|
||||
})
|
||||
|
||||
go func() {
|
||||
defer utils.CatchPanicWithCancel(cancelMain)
|
||||
if *x.Config.Conf.TLS.TlsEnabled {
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", *x.Config.Conf.HTTPServer.Address, *x.Config.Conf.HTTPServer.Port))
|
||||
if err != nil {
|
||||
x.Log.Printf("%s: Failed to start TLS listener: %s", colors.PrintError(), err.Error())
|
||||
cancelMain()
|
||||
return
|
||||
}
|
||||
x.Log.Printf("Serving on %s port %s with TLS... (https://%s%s)", *x.Config.Conf.HTTPServer.Address, *x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", *x.Config.Conf.HTTPServer.Address, *x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
|
||||
limitedListener := netutil.LimitListener(listener, 100)
|
||||
if err := srv.ServeTLS(limitedListener, *x.Config.Conf.TLS.CertFile, *x.Config.Conf.TLS.KeyFile); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
x.Log.Printf("%s: Failed to start HTTPS server: %s", colors.PrintError(), err.Error())
|
||||
cancelMain()
|
||||
}
|
||||
} else {
|
||||
x.Log.Printf("Serving on %s port %s... (http://%s%s)", *x.Config.Conf.HTTPServer.Address, *x.Config.Conf.HTTPServer.Port, fmt.Sprintf("%s:%s", *x.Config.Conf.HTTPServer.Address, *x.Config.Conf.HTTPServer.Port), config.ComDirRoute)
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", *x.Config.Conf.HTTPServer.Address, *x.Config.Conf.HTTPServer.Port))
|
||||
if err != nil {
|
||||
x.Log.Printf("%s: Failed to start listener: %s", colors.PrintError(), err.Error())
|
||||
cancelMain()
|
||||
return
|
||||
}
|
||||
limitedListener := netutil.LimitListener(listener, 100)
|
||||
if err := srv.Serve(limitedListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
x.Log.Printf("%s: Failed to start HTTP server: %s", colors.PrintError(), err.Error())
|
||||
cancelMain()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
session_manager.StartCleanup(5 * time.Second)
|
||||
|
||||
if *x.Config.Conf.Updates.UpdatesEnabled {
|
||||
go func() {
|
||||
defer utils.CatchPanicWithCancel(cancelMain)
|
||||
updated := update.NewUpdater(&update.UpdaterInit{
|
||||
X: x,
|
||||
Ctx: ctxMain,
|
||||
Cancel: cancelMain,
|
||||
})
|
||||
updated.Shutdownfunc(cancelMain)
|
||||
for {
|
||||
isNewUpdate, err := updated.CkeckUpdates()
|
||||
if err != nil {
|
||||
x.Log.Printf("Failed to check for updates: %s", err.Error())
|
||||
}
|
||||
if isNewUpdate {
|
||||
if err := updated.Update(); err != nil {
|
||||
x.Log.Printf("Failed to update: %s", err.Error())
|
||||
} else {
|
||||
x.Log.Printf("Update completed successfully")
|
||||
}
|
||||
}
|
||||
time.Sleep(*x.Config.Conf.Updates.CheckInterval)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
<-ctxMain.Done()
|
||||
NodeApp.CallFallback(ctx)
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package logs
|
||||
package colors
|
||||
|
||||
import "fmt"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package corestate
|
||||
|
||||
var NODE_UUID string
|
||||
|
||||
type Stage string
|
||||
|
||||
const (
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
|
||||
)
|
||||
|
||||
// GetNodeUUID outputs the correct uuid from the file at the path specified in the arguments.
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
|
||||
)
|
||||
|
||||
type RunManagerContract interface {
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -14,9 +13,10 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/run_manager"
|
||||
"github.com/akyaiy/GoSally-mvp/core/utils"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/run_manager"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@@ -38,20 +38,23 @@ type UpdaterContract interface {
|
||||
}
|
||||
|
||||
type Updater struct {
|
||||
log *log.Logger
|
||||
config *config.Conf
|
||||
env *config.Env
|
||||
x *app.AppX
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewUpdater(ctx context.Context, log *log.Logger, cfg *config.Conf, env *config.Env) *Updater {
|
||||
type UpdaterInit struct {
|
||||
X *app.AppX
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewUpdater(o *UpdaterInit) *Updater {
|
||||
return &Updater{
|
||||
log: log,
|
||||
config: cfg,
|
||||
env: env,
|
||||
ctx: ctx,
|
||||
x: o.X,
|
||||
ctx: o.Ctx,
|
||||
cancel: o.Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +122,7 @@ func isVersionNewer(current, latest Version) bool {
|
||||
func (u *Updater) GetCurrentVersion() (Version, Branch, error) {
|
||||
version, branch, err := splitVersionString(string(config.NodeVersion))
|
||||
if err != nil {
|
||||
u.log.Printf("Failed to parse version string: %s", err.Error())
|
||||
u.x.Log.Printf("Failed to parse version string: %s", err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
switch branch {
|
||||
@@ -131,28 +134,28 @@ func (u *Updater) GetCurrentVersion() (Version, Branch, error) {
|
||||
}
|
||||
|
||||
func (u *Updater) GetLatestVersion(updateBranch Branch) (Version, Branch, error) {
|
||||
repoURL := u.config.Updates.RepositoryURL
|
||||
repoURL := *u.x.Config.Conf.Updates.RepositoryURL
|
||||
if repoURL == "" {
|
||||
u.log.Printf("Failed to get latest version: %s", "RepositoryURL is empty in config")
|
||||
u.x.Log.Printf("Failed to get latest version: %s", "RepositoryURL is empty in config")
|
||||
return "", "", errors.New("repository URL is empty")
|
||||
}
|
||||
if !strings.HasPrefix(repoURL, "http://") && !strings.HasPrefix(repoURL, "https://") {
|
||||
u.log.Printf("Failed to get latest version: %s: %s", "RepositoryURL does not start with http:// or https:/", repoURL)
|
||||
u.x.Log.Printf("Failed to get latest version: %s: %s", "RepositoryURL does not start with http:// or https:/", repoURL)
|
||||
return "", "", errors.New("repository URL must start with http:// or https://")
|
||||
}
|
||||
response, err := http.Get(repoURL + "/" + config.ActualFileName)
|
||||
if err != nil {
|
||||
u.log.Printf("Failed to fetch latest version: %s", err.Error())
|
||||
u.x.Log.Printf("Failed to fetch latest version: %s", err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
u.log.Printf("Failed to fetch latest version: HTTP status %d", response.StatusCode)
|
||||
u.x.Log.Printf("Failed to fetch latest version: HTTP status %d", response.StatusCode)
|
||||
return "", "", errors.New("failed to fetch latest version, status code: " + http.StatusText(response.StatusCode))
|
||||
}
|
||||
data, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
u.log.Printf("Failed to read latest version response: %s", err.Error())
|
||||
u.x.Log.Printf("Failed to read latest version response: %s", err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
@@ -163,7 +166,7 @@ func (u *Updater) GetLatestVersion(updateBranch Branch) (Version, Branch, error)
|
||||
}
|
||||
version, branch, err := splitVersionString(string(line))
|
||||
if err != nil {
|
||||
u.log.Printf("Failed to parse version string: %s", err.Error())
|
||||
u.x.Log.Printf("Failed to parse version string: %s", err.Error())
|
||||
return "", "", err
|
||||
}
|
||||
if branch == updateBranch {
|
||||
@@ -189,7 +192,7 @@ func (u *Updater) CkeckUpdates() (IsNewUpdate, error) {
|
||||
}
|
||||
|
||||
func (u *Updater) Update() error {
|
||||
if !u.config.Updates.UpdatesEnabled {
|
||||
if !*u.x.Config.Conf.Updates.UpdatesEnabled {
|
||||
return errors.New("updates are disabled in config, skipping update")
|
||||
}
|
||||
|
||||
@@ -209,7 +212,7 @@ func (u *Updater) Update() error {
|
||||
}
|
||||
|
||||
updateArchiveName := fmt.Sprintf("%s.v%s-%s", config.UpdateArchiveName, latestVersion, latestBranch)
|
||||
updateDest := fmt.Sprintf("%s/%s.%s", u.config.Updates.RepositoryURL, updateArchiveName, "tar.gz")
|
||||
updateDest := fmt.Sprintf("%s/%s.%s", *u.x.Config.Conf.Updates.RepositoryURL, updateArchiveName, "tar.gz")
|
||||
|
||||
resp, err := http.Get(updateDest)
|
||||
if err != nil {
|
||||
@@ -275,7 +278,7 @@ func (u *Updater) Update() error {
|
||||
|
||||
func (u *Updater) InstallAndRestart() error {
|
||||
|
||||
nodePath := u.env.NodePath
|
||||
nodePath := *u.x.Config.Env.NodePath
|
||||
if nodePath == "" {
|
||||
return errors.New("GS_NODE_PATH environment variable is not set")
|
||||
}
|
||||
@@ -303,12 +306,7 @@ func (u *Updater) InstallAndRestart() error {
|
||||
return fmt.Errorf("failed to chmod: %w", err)
|
||||
}
|
||||
|
||||
u.log.Printf("Launching new version: path is %s", targetPath)
|
||||
// cmd := exec.Command(targetPath, os.Args[1:]...)
|
||||
// cmd.Env = os.Environ()
|
||||
// cmd.Stdout = os.Stdout
|
||||
// cmd.Stderr = os.Stderr
|
||||
// cmd.Stdin = os.Stdin
|
||||
u.x.Log.Printf("Launching new version: path is %s", targetPath)
|
||||
args := os.Args
|
||||
args[0] = targetPath
|
||||
env := utils.SetEviron(os.Environ(), "GS_PARENT_PID=-1")
|
||||
@@ -317,17 +315,6 @@ func (u *Updater) InstallAndRestart() error {
|
||||
return err
|
||||
}
|
||||
return syscall.Exec(targetPath, args, env)
|
||||
//u.cancel()
|
||||
|
||||
// TODO: fix this crap and find a better way to update without errors
|
||||
// for {
|
||||
// _, err := run_manager.Get("run.lock")
|
||||
// if err != nil {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// return cmd.Start()
|
||||
}
|
||||
|
||||
func (u *Updater) Shutdownfunc(f context.CancelFunc) {
|
||||
34
internal/core/utils/panic.go
Normal file
34
internal/core/utils/panic.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func CatchPanic() {
|
||||
if err := recover(); err != nil {
|
||||
stack := make([]byte, 8096)
|
||||
stack = stack[:runtime.Stack(stack, false)]
|
||||
log.Printf("recovered panic:\n%s", stack)
|
||||
}
|
||||
}
|
||||
|
||||
func CatchPanicWithCancel(cancel context.CancelFunc) {
|
||||
if err := recover(); err != nil {
|
||||
stack := make([]byte, 8096)
|
||||
stack = stack[:runtime.Stack(stack, false)]
|
||||
log.Printf("recovered panic:\n%s", stack)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func CatchPanicWithFallback(onPanic func(any)) {
|
||||
if err := recover(); err != nil {
|
||||
stack := make([]byte, 8096)
|
||||
stack = stack[:runtime.Stack(stack, false)]
|
||||
log.Printf("recovered panic:\n%s", stack)
|
||||
onPanic(err)
|
||||
}
|
||||
}
|
||||
9
internal/core/utils/safe_fetch.go
Normal file
9
internal/core/utils/safe_fetch.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package utils
|
||||
|
||||
// SafeFetch safely fetches data. If v = nil, a fallback value is returned.
|
||||
func SafeFetch[T any](v *T, fallback T) T {
|
||||
if v == nil {
|
||||
return fallback
|
||||
}
|
||||
return *v
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
|
||||
)
|
||||
|
||||
func NewUUIDRaw(length int) ([]byte, error) {
|
||||
95
internal/engine/app/app.go
Normal file
95
internal/engine/app/app.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
|
||||
)
|
||||
|
||||
type AppContract interface {
|
||||
InitialHooks(fn ...func(ctx context.Context, cs *corestate.CoreState, x *AppX))
|
||||
Run(fn func(ctx context.Context, cs *corestate.CoreState, x *AppX) error)
|
||||
Fallback(fn func(ctx context.Context, cs *corestate.CoreState, x *AppX))
|
||||
|
||||
CallFallback(ctx context.Context)
|
||||
}
|
||||
|
||||
type App struct {
|
||||
initHooks []func(ctx context.Context, cs *corestate.CoreState, x *AppX)
|
||||
runHook func(ctx context.Context, cs *corestate.CoreState, x *AppX) error
|
||||
fallback func(ctx context.Context, cs *corestate.CoreState, x *AppX)
|
||||
|
||||
Corestate *corestate.CoreState
|
||||
AppX *AppX
|
||||
|
||||
fallbackOnce sync.Once
|
||||
}
|
||||
|
||||
type AppX struct {
|
||||
Config *config.Compositor
|
||||
Log *log.Logger
|
||||
SLog *slog.Logger
|
||||
}
|
||||
|
||||
func New() AppContract {
|
||||
return &App{
|
||||
AppX: &AppX{
|
||||
Log: log.Default(),
|
||||
},
|
||||
Corestate: &corestate.CoreState{},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) InitialHooks(fn ...func(ctx context.Context, cs *corestate.CoreState, x *AppX)) {
|
||||
a.initHooks = append(a.initHooks, fn...)
|
||||
}
|
||||
|
||||
func (a *App) Fallback(fn func(ctx context.Context, cs *corestate.CoreState, x *AppX)) {
|
||||
a.fallback = fn
|
||||
}
|
||||
|
||||
func (a *App) Run(fn func(ctx context.Context, cs *corestate.CoreState, x *AppX) error) {
|
||||
a.runHook = fn
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||
defer stop()
|
||||
|
||||
for _, hook := range a.initHooks {
|
||||
hook(ctx, a.Corestate, a.AppX)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
a.AppX.Log.Printf("PANIC recovered: %v", r)
|
||||
if a.fallback != nil {
|
||||
a.fallback(ctx, a.Corestate, a.AppX)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
var runErr error
|
||||
if a.runHook != nil {
|
||||
runErr = a.runHook(ctx, a.Corestate, a.AppX)
|
||||
}
|
||||
|
||||
if runErr != nil {
|
||||
a.AppX.Log.Fatalf("fatal in Run: %v", runErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) CallFallback(ctx context.Context) {
|
||||
a.fallbackOnce.Do(func() {
|
||||
if a.fallback != nil {
|
||||
a.fallback(ctx, a.Corestate, a.AppX)
|
||||
}
|
||||
os.Exit(0)
|
||||
})
|
||||
}
|
||||
@@ -43,10 +43,13 @@ func (c *Compositor) LoadConf(path string) error {
|
||||
v.SetConfigType("yaml")
|
||||
|
||||
// defaults
|
||||
v.SetDefault("mode", "dev")
|
||||
v.SetDefault("com_dir", "./com/")
|
||||
v.SetDefault("node.name", "noname")
|
||||
v.SetDefault("node.mode", "dev")
|
||||
v.SetDefault("node.show_config", "false")
|
||||
v.SetDefault("node.com_dir", "./com/")
|
||||
v.SetDefault("http_server.address", "0.0.0.0")
|
||||
v.SetDefault("http_server.port", "8080")
|
||||
v.SetDefault("http_server.session_ttl", "30m")
|
||||
v.SetDefault("http_server.timeout", "5s")
|
||||
v.SetDefault("http_server.idle_timeout", "60s")
|
||||
v.SetDefault("tls.enabled", false)
|
||||
@@ -55,8 +58,9 @@ func (c *Compositor) LoadConf(path string) error {
|
||||
v.SetDefault("updates.enabled", false)
|
||||
v.SetDefault("updates.check_interval", "2h")
|
||||
v.SetDefault("updates.wanted_version", "latest-stable")
|
||||
v.SetDefault("log.json_format", "false")
|
||||
v.SetDefault("log.level", "info")
|
||||
v.SetDefault("log.out_path", "")
|
||||
v.SetDefault("log.output", "%2%")
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return fmt.Errorf("error reading config: %w", err)
|
||||
82
internal/engine/config/config.go
Normal file
82
internal/engine/config/config.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Package config provides configuration management for the application.
|
||||
// config is built on top of the third-party module cleanenv
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type CompositorContract interface {
|
||||
LoadEnv() error
|
||||
LoadConf(path string) error
|
||||
}
|
||||
|
||||
type Compositor struct {
|
||||
CMDLine *CMDLine
|
||||
Conf *Conf
|
||||
Env *Env
|
||||
}
|
||||
|
||||
type Conf struct {
|
||||
Node *Node `mapstructure:"node"`
|
||||
HTTPServer *HTTPServer `mapstructure:"http_server"`
|
||||
TLS *TLS `mapstructure:"tls"`
|
||||
Updates *Updates `mapstructure:"updates"`
|
||||
Log *Log `mapstructure:"log"`
|
||||
DisableWarnings *[]string `mapstructure:"disable_warnings"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Mode *string `mapstructure:"mode"`
|
||||
Name *string `mapstructure:"name"`
|
||||
ShowConfig *bool `mapstructure:"show_config"`
|
||||
ComDir *string `mapstructure:"com_dir"`
|
||||
}
|
||||
|
||||
type HTTPServer struct {
|
||||
Address *string `mapstructure:"address"`
|
||||
Port *string `mapstructure:"port"`
|
||||
SessionTTL *time.Duration `mapstructure:"session_ttl"`
|
||||
Timeout *time.Duration `mapstructure:"timeout"`
|
||||
IdleTimeout *time.Duration `mapstructure:"idle_timeout"`
|
||||
}
|
||||
|
||||
type TLS struct {
|
||||
TlsEnabled *bool `mapstructure:"enabled"`
|
||||
CertFile *string `mapstructure:"cert_file"`
|
||||
KeyFile *string `mapstructure:"key_file"`
|
||||
}
|
||||
|
||||
type Updates struct {
|
||||
UpdatesEnabled *bool `mapstructure:"enabled"`
|
||||
CheckInterval *time.Duration `mapstructure:"check_interval"`
|
||||
RepositoryURL *string `mapstructure:"repository_url"`
|
||||
WantedVersion *string `mapstructure:"wanted_version"`
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
JSON *bool `mapstructure:"json_format"`
|
||||
Level *string `mapstructure:"level"`
|
||||
OutPath *string `mapstructure:"output"`
|
||||
}
|
||||
|
||||
// ConfigEnv structure for environment variables
|
||||
type Env struct {
|
||||
ConfigPath *string `mapstructure:"config_path"`
|
||||
NodePath *string `mapstructure:"node_path"`
|
||||
ParentStagePID *int `mapstructure:"parent_pid"`
|
||||
}
|
||||
|
||||
type CMDLine struct {
|
||||
Run Run
|
||||
Node Root
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
Debug bool `persistent:"true" full:"debug" short:"d" def:"false" desc:"Set debug mode"`
|
||||
}
|
||||
|
||||
type Run struct {
|
||||
ConfigPath string `persistent:"true" full:"config" short:"c" def:"./config.yaml" desc:"Path to configuration file"`
|
||||
Test []int `persistent:"true" full:"test" short:"t" def:"" desc:"js test"`
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package config
|
||||
|
||||
import "os"
|
||||
|
||||
// TODO: Need to make a more harmonious and understandable way of storing global variables
|
||||
|
||||
// UUIDLength is uuids length for sessions. By default it is 16 bytes.
|
||||
var UUIDLength int = 16
|
||||
|
||||
72
internal/engine/config/print.go
Normal file
72
internal/engine/config/print.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/colors"
|
||||
)
|
||||
|
||||
func (c *Compositor) Print(v any) {
|
||||
c.printConfig(v, " ")
|
||||
}
|
||||
|
||||
func (c *Compositor) printConfig(v any, prefix string) {
|
||||
val := reflect.ValueOf(v)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
typ := val.Type()
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
|
||||
fieldName := fieldType.Name
|
||||
if tag, ok := fieldType.Tag.Lookup("mapstructure"); ok {
|
||||
if tag != "" {
|
||||
fieldName = tag
|
||||
}
|
||||
}
|
||||
|
||||
coloredFieldName := colors.SetBrightCyan(fieldName)
|
||||
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if field.IsNil() {
|
||||
fmt.Printf("%s%s: %s\n", prefix, coloredFieldName, colors.SetBrightRed("<nil>"))
|
||||
continue
|
||||
}
|
||||
field = field.Elem()
|
||||
}
|
||||
|
||||
if field.Kind() == reflect.Struct {
|
||||
if field.Type() == reflect.TypeOf(time.Duration(0)) {
|
||||
duration := field.Interface().(time.Duration)
|
||||
fmt.Printf("%s%s: %s\n",
|
||||
prefix,
|
||||
coloredFieldName,
|
||||
colors.SetBrightYellow(duration.String()))
|
||||
} else {
|
||||
fmt.Printf("%s%s:\n", prefix, coloredFieldName)
|
||||
c.printConfig(field.Addr().Interface(), prefix+" ")
|
||||
}
|
||||
} else if field.Kind() == reflect.Slice {
|
||||
fmt.Printf("%s%s: %s\n",
|
||||
prefix,
|
||||
coloredFieldName,
|
||||
colors.SetBrightYellow(fmt.Sprintf("%v", field.Interface())))
|
||||
} else {
|
||||
value := field.Interface()
|
||||
valueStr := fmt.Sprintf("%v", value)
|
||||
if field.Kind() == reflect.String {
|
||||
valueStr = fmt.Sprintf("\"%s\"", value)
|
||||
}
|
||||
fmt.Printf("%s%s: %s\n",
|
||||
prefix,
|
||||
coloredFieldName,
|
||||
colors.SetBrightYellow(valueStr))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,25 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/core/run_manager"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var GlobalLevel slog.Level
|
||||
|
||||
type levelsStruct struct {
|
||||
Available []string
|
||||
Fallback string
|
||||
}
|
||||
|
||||
var Levels = levelsStruct{
|
||||
Available: []string{
|
||||
"debug", "info",
|
||||
},
|
||||
Fallback: "info",
|
||||
}
|
||||
|
||||
type SlogWriter struct {
|
||||
Logger *slog.Logger
|
||||
Level slog.Level
|
||||
@@ -32,11 +41,11 @@ func (w *SlogWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
// SetupLogger initializes and returns a logger based on the provided environment.
|
||||
func SetupLogger(o config.Log) (*slog.Logger, error) {
|
||||
func SetupLogger(o *config.Log) (*slog.Logger, error) {
|
||||
var handlerOpts = slog.HandlerOptions{}
|
||||
var writer io.Writer = os.Stdout
|
||||
|
||||
switch o.Level {
|
||||
switch *o.Level {
|
||||
case "debug":
|
||||
GlobalLevel = slog.LevelDebug
|
||||
handlerOpts.Level = slog.LevelDebug
|
||||
@@ -48,32 +57,14 @@ func SetupLogger(o config.Log) (*slog.Logger, error) {
|
||||
handlerOpts.Level = slog.LevelInfo
|
||||
}
|
||||
|
||||
if o.OutPath != "" {
|
||||
repl := map[string]string{
|
||||
"tmp": filepath.Clean(run_manager.RuntimeDir()),
|
||||
}
|
||||
re := regexp.MustCompile(`%(\w+)%`)
|
||||
result := re.ReplaceAllStringFunc(o.OutPath, func(match string) string {
|
||||
sub := re.FindStringSubmatch(match)
|
||||
if len(sub) < 2 {
|
||||
return match
|
||||
}
|
||||
key := sub[1]
|
||||
if val, ok := repl[key]; ok {
|
||||
return val
|
||||
}
|
||||
return match
|
||||
})
|
||||
|
||||
if strings.Contains(o.OutPath, "%tmp%") {
|
||||
relPath := strings.TrimPrefix(result, filepath.Clean(run_manager.RuntimeDir()))
|
||||
if err := run_manager.SetDir(relPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
switch *o.OutPath {
|
||||
case "_1STDout":
|
||||
writer = os.Stdout
|
||||
case "_2STDerr":
|
||||
writer = os.Stderr
|
||||
default:
|
||||
logFile := &lumberjack.Logger{
|
||||
Filename: filepath.Join(result, "event.log"),
|
||||
Filename: filepath.Join(*o.OutPath, "event.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 5,
|
||||
MaxAge: 28,
|
||||
@@ -82,6 +73,13 @@ func SetupLogger(o config.Log) (*slog.Logger, error) {
|
||||
writer = logFile
|
||||
}
|
||||
|
||||
log := slog.New(slog.NewJSONHandler(writer, &handlerOpts))
|
||||
var handler slog.Handler
|
||||
|
||||
if *o.JSON {
|
||||
handler = slog.NewJSONHandler(writer, &handlerOpts)
|
||||
} else {
|
||||
handler = slog.NewTextHandler(writer, &handlerOpts)
|
||||
}
|
||||
log := slog.New(handler)
|
||||
return log, nil
|
||||
}
|
||||
1
internal/engine/lua/handler.go
Normal file
1
internal/engine/lua/handler.go
Normal file
@@ -0,0 +1 @@
|
||||
package lua
|
||||
35
internal/engine/lua/pool.go
Normal file
35
internal/engine/lua/pool.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type LuaPool struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
func NewLuaPool() *LuaPool {
|
||||
return &LuaPool{
|
||||
pool: sync.Pool{
|
||||
New: func() any {
|
||||
L := lua.NewState()
|
||||
|
||||
return L
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (lp *LuaPool) Get() *lua.LState {
|
||||
return lp.pool.Get().(*lua.LState)
|
||||
}
|
||||
|
||||
func (lp *LuaPool) Put(L *lua.LState) {
|
||||
L.Close()
|
||||
|
||||
newL := lua.NewState()
|
||||
|
||||
lp.pool.Put(newL)
|
||||
}
|
||||
26
internal/engine/lua/types.go
Normal file
26
internal/engine/lua/types.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package lua
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
|
||||
)
|
||||
|
||||
type LuaEngineDeps struct {
|
||||
HttpRequest *http.Request
|
||||
JSONRPCRequest *rpc.RPCRequest
|
||||
SessionUUID string
|
||||
ScriptPath string
|
||||
}
|
||||
|
||||
type LuaEngineContract interface {
|
||||
Handle(deps *LuaEngineDeps) *rpc.RPCResponse
|
||||
|
||||
}
|
||||
|
||||
type LuaEngine struct {
|
||||
x *app.AppX
|
||||
cs *corestate.CoreState
|
||||
}
|
||||
30
internal/server/gateway/general_types.go
Normal file
30
internal/server/gateway/general_types.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/session"
|
||||
)
|
||||
|
||||
// serversApiVer is a type alias for string, used to represent API version strings in the GeneralServer.
|
||||
type serversApiVer string
|
||||
|
||||
type ServerApiContract interface {
|
||||
GetVersion() string
|
||||
Handle(ctx context.Context, sid string, r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse
|
||||
}
|
||||
|
||||
// GeneralServer implements the GeneralServerApiContract and serves as a router for different API versions.
|
||||
type GatewayServer struct {
|
||||
// servers holds the registered servers by their API version.
|
||||
// The key is the version string, and the value is the server implementing GeneralServerApi
|
||||
servers map[serversApiVer]ServerApiContract
|
||||
|
||||
sm *session.SessionManager
|
||||
cs *corestate.CoreState
|
||||
x *app.AppX
|
||||
}
|
||||
47
internal/server/gateway/init.go
Normal file
47
internal/server/gateway/init.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/session"
|
||||
)
|
||||
|
||||
// GeneralServerInit structure only for initialization general server.
|
||||
type GatewayServerInit struct {
|
||||
SM *session.SessionManager
|
||||
CS *corestate.CoreState
|
||||
X *app.AppX
|
||||
}
|
||||
|
||||
// InitGeneral initializes a new GeneralServer with the provided configuration and registered servers.
|
||||
func InitGateway(o *GatewayServerInit, servers ...ServerApiContract) *GatewayServer {
|
||||
general := &GatewayServer{
|
||||
servers: make(map[serversApiVer]ServerApiContract),
|
||||
sm: o.SM,
|
||||
cs: o.CS,
|
||||
x: o.X,
|
||||
}
|
||||
|
||||
// register the provided servers
|
||||
// s is each server implementing GeneralServerApiContract, this is not a general server
|
||||
for _, s := range servers {
|
||||
general.servers[serversApiVer(s.GetVersion())] = s
|
||||
}
|
||||
return general
|
||||
}
|
||||
|
||||
// GetVersion returns the API version of the GeneralServer, which is "general".
|
||||
func (s *GatewayServer) GetVersion() string {
|
||||
return "general"
|
||||
}
|
||||
|
||||
// AppendToArray adds a new server to the GeneralServer's internal map.
|
||||
func (s *GatewayServer) AppendToArray(server ServerApiContract) 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")
|
||||
}
|
||||
114
internal/server/gateway/route.go
Normal file
114
internal/server/gateway/route.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context() // TODO
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
sessionUUID := r.Header.Get("X-Session-UUID")
|
||||
if sessionUUID == "" {
|
||||
sessionUUID = uuid.New().String()
|
||||
|
||||
}
|
||||
gs.x.SLog.Debug("new request", slog.String("session-uuid", sessionUUID), slog.Group("connection", slog.String("ip", r.RemoteAddr)))
|
||||
|
||||
w.Header().Set("X-Session-UUID", sessionUUID)
|
||||
if !gs.sm.Add(sessionUUID) {
|
||||
gs.x.SLog.Debug("session is busy", slog.String("session-uuid", sessionUUID))
|
||||
rpc.WriteError(w, rpc.NewError(rpc.ErrSessionIsBusy, rpc.ErrSessionIsBusyS, nil, nil))
|
||||
return
|
||||
}
|
||||
defer gs.sm.Delete(sessionUUID)
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
gs.x.SLog.Debug("failed to read body", slog.String("err", err.Error()))
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
rpc.WriteError(w, rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, nil))
|
||||
gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrInternalErrorS))
|
||||
return
|
||||
}
|
||||
|
||||
// determine if the JSON-RPC request is a batch
|
||||
var batch []rpc.RPCRequest
|
||||
json.Unmarshal(body, &batch)
|
||||
var single rpc.RPCRequest
|
||||
if batch == nil {
|
||||
if err := json.Unmarshal(body, &single); err != nil {
|
||||
gs.x.SLog.Debug("failed to parse json", slog.String("err", err.Error()))
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
rpc.WriteError(w, rpc.NewError(rpc.ErrParseError, rpc.ErrParseErrorS, nil, nil))
|
||||
gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrParseErrorS))
|
||||
return
|
||||
}
|
||||
resp := gs.Route(ctx, sessionUUID, r, &single)
|
||||
if resp == nil {
|
||||
w.Write([]byte(""))
|
||||
return
|
||||
}
|
||||
rpc.WriteResponse(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// handle batch
|
||||
responses := make(chan rpc.RPCResponse, len(batch))
|
||||
var wg sync.WaitGroup
|
||||
for _, m := range batch {
|
||||
wg.Add(1)
|
||||
go func(req rpc.RPCRequest) {
|
||||
defer wg.Done()
|
||||
res := gs.Route(ctx, sessionUUID, r, &req)
|
||||
if res != nil {
|
||||
responses <- *res
|
||||
}
|
||||
}(m)
|
||||
}
|
||||
wg.Wait()
|
||||
close(responses)
|
||||
|
||||
var result []rpc.RPCResponse
|
||||
for res := range responses {
|
||||
result = append(result, res)
|
||||
}
|
||||
if len(result) > 0 {
|
||||
json.NewEncoder(w).Encode(result)
|
||||
} else {
|
||||
w.Write([]byte("[]"))
|
||||
}
|
||||
}
|
||||
|
||||
func (gs *GatewayServer) Route(ctx context.Context, sid string, r *http.Request, req *rpc.RPCRequest) (resp *rpc.RPCResponse) {
|
||||
defer utils.CatchPanicWithFallback(func(rec any) {
|
||||
gs.x.SLog.Error("panic caught in handler", slog.Any("error", rec))
|
||||
resp = rpc.NewError(rpc.ErrInternalError, "Internal server error (panic)", nil, req.ID)
|
||||
})
|
||||
if req.JSONRPC != rpc.JSONRPCVersion {
|
||||
gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrInvalidRequestS), slog.String("requested-version", req.JSONRPC))
|
||||
return rpc.NewError(rpc.ErrInvalidRequest, rpc.ErrInvalidRequestS, nil, req.ID)
|
||||
}
|
||||
|
||||
server, ok := gs.servers[serversApiVer(req.ContextVersion)]
|
||||
if !ok {
|
||||
gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrContextVersionS), slog.String("requested-version", req.ContextVersion))
|
||||
return rpc.NewError(rpc.ErrContextVersion, rpc.ErrContextVersionS, nil, req.ID)
|
||||
}
|
||||
|
||||
// checks if request is notification
|
||||
if req.ID == nil {
|
||||
go server.Handle(ctx, sid, r, req)
|
||||
return nil
|
||||
}
|
||||
return server.Handle(ctx, sid, r, req)
|
||||
}
|
||||
30
internal/server/rpc/definition.go
Normal file
30
internal/server/rpc/definition.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package rpc
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type RPCRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID *json.RawMessage `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params any `json:"params,omitempty"`
|
||||
ContextVersion string `json:"context-version,omitempty"`
|
||||
}
|
||||
|
||||
type RPCResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID *json.RawMessage `json:"id"`
|
||||
Result any `json:"result,omitzero"`
|
||||
Error any `json:"error,omitzero"`
|
||||
Data *RPCData `json:"data,omitzero"`
|
||||
}
|
||||
|
||||
type RPCData struct {
|
||||
ResponsibleNode string `json:"responsible-node,omitempty"`
|
||||
Salt string `json:"salt,omitempty"`
|
||||
Checksum string `json:"checksum-md5,omitempty"`
|
||||
NewSessionUUID string `json:"new-session-uuid,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
JSONRPCVersion = "2.0"
|
||||
)
|
||||
30
internal/server/rpc/errors.go
Normal file
30
internal/server/rpc/errors.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package rpc
|
||||
|
||||
const (
|
||||
ErrParseError = -32700
|
||||
ErrParseErrorS = "Parse error"
|
||||
|
||||
ErrInvalidRequest = -32600
|
||||
ErrInvalidRequestS = "Invalid Request"
|
||||
|
||||
ErrMethodNotFound = -32601
|
||||
ErrMethodNotFoundS = "Method not found"
|
||||
|
||||
ErrInvalidParams = -32602
|
||||
ErrInvalidParamsS = "Invalid params"
|
||||
|
||||
ErrInternalError = -32603
|
||||
ErrInternalErrorS = "Internal error"
|
||||
|
||||
ErrContextVersion = -32010
|
||||
ErrContextVersionS = "Invalid context version"
|
||||
|
||||
ErrInvalidMethodFormat = -32020
|
||||
ErrInvalidMethodFormatS = "Invalid method format"
|
||||
|
||||
ErrMethodIsMissing = -32020
|
||||
ErrMethodIsMissingS = "Method is missing"
|
||||
|
||||
ErrSessionIsBusy = -32030
|
||||
ErrSessionIsBusyS = "The session is busy"
|
||||
)
|
||||
58
internal/server/rpc/responsers.go
Normal file
58
internal/server/rpc/responsers.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func generateChecksum(result any) string {
|
||||
if result == nil {
|
||||
return ""
|
||||
}
|
||||
data, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%x", md5.Sum(data))
|
||||
}
|
||||
|
||||
func generateSalt() string {
|
||||
return uuid.NewString()
|
||||
}
|
||||
|
||||
func GetData(data any) *RPCData {
|
||||
return &RPCData{
|
||||
Salt: generateSalt(),
|
||||
ResponsibleNode: corestate.NODE_UUID,
|
||||
Checksum: generateChecksum(data),
|
||||
}
|
||||
}
|
||||
|
||||
func NewError(code int, message string, data any, id *json.RawMessage) *RPCResponse {
|
||||
Error := make(map[string]any)
|
||||
Error = map[string]any{
|
||||
"code": code,
|
||||
"message": message,
|
||||
"data": data,
|
||||
}
|
||||
|
||||
return &RPCResponse{
|
||||
JSONRPC: JSONRPCVersion,
|
||||
ID: id,
|
||||
Error: Error,
|
||||
Data: GetData(Error),
|
||||
}
|
||||
}
|
||||
|
||||
func NewResponse(result any, id *json.RawMessage) *RPCResponse {
|
||||
return &RPCResponse{
|
||||
JSONRPC: JSONRPCVersion,
|
||||
ID: id,
|
||||
Result: result,
|
||||
Data: GetData(result),
|
||||
}
|
||||
}
|
||||
23
internal/server/rpc/writers.go
Normal file
23
internal/server/rpc/writers.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func write(w http.ResponseWriter, msg *RPCResponse) error {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, errm *RPCResponse) error {
|
||||
return write(w, errm)
|
||||
}
|
||||
|
||||
func WriteResponse(w http.ResponseWriter, response *RPCResponse) error {
|
||||
return write(w, response)
|
||||
}
|
||||
47
internal/server/session/manager.go
Normal file
47
internal/server/session/manager.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SessionManagerContract interface {
|
||||
Add(uuid string) bool
|
||||
Delete(uuid string)
|
||||
StartCleanup(interval time.Duration)
|
||||
}
|
||||
|
||||
type SessionManager struct {
|
||||
sessions sync.Map
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
func New(ttl time.Duration) *SessionManager {
|
||||
return &SessionManager{
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *SessionManager) Add(uuid string) bool {
|
||||
_, loaded := sm.sessions.LoadOrStore(uuid, time.Now().Add(sm.ttl))
|
||||
return !loaded
|
||||
}
|
||||
|
||||
func (sm *SessionManager) Delete(uuid string) {
|
||||
sm.sessions.Delete(uuid)
|
||||
}
|
||||
|
||||
func (sm *SessionManager) StartCleanup(interval time.Duration) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(interval)
|
||||
for range ticker.C {
|
||||
sm.sessions.Range(func(key, value any) bool {
|
||||
expiry := value.(time.Time)
|
||||
if time.Now().After(expiry) {
|
||||
sm.sessions.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
415
internal/server/sv1/db_sqlite.go
Normal file
415
internal/server/sv1/db_sqlite.go
Normal file
@@ -0,0 +1,415 @@
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type DBConnection struct {
|
||||
dbPath string
|
||||
log bool
|
||||
logger *slog.Logger
|
||||
writeChan chan *dbWriteRequest
|
||||
closeChan chan struct{}
|
||||
}
|
||||
|
||||
type dbWriteRequest struct {
|
||||
query string
|
||||
args []interface{}
|
||||
resCh chan *dbWriteResult
|
||||
}
|
||||
|
||||
type dbWriteResult struct {
|
||||
rowsAffected int64
|
||||
err error
|
||||
}
|
||||
|
||||
var dbMutexMap = make(map[string]*sync.RWMutex)
|
||||
var dbGlobalMutex sync.Mutex
|
||||
|
||||
func getDBMutex(dbPath string) *sync.RWMutex {
|
||||
dbGlobalMutex.Lock()
|
||||
defer dbGlobalMutex.Unlock()
|
||||
|
||||
if mtx, ok := dbMutexMap[dbPath]; ok {
|
||||
return mtx
|
||||
}
|
||||
|
||||
mtx := &sync.RWMutex{}
|
||||
dbMutexMap[dbPath] = mtx
|
||||
return mtx
|
||||
}
|
||||
|
||||
func loadDBMod(llog *slog.Logger, sid string) func(*lua.LState) int {
|
||||
return func(L *lua.LState) int {
|
||||
llog.Debug("import module db-sqlite")
|
||||
dbMod := L.NewTable()
|
||||
|
||||
L.SetField(dbMod, "connect", L.NewFunction(func(L *lua.LState) int {
|
||||
dbPath := L.CheckString(1)
|
||||
|
||||
logQueries := false
|
||||
if L.GetTop() >= 2 {
|
||||
opts := L.CheckTable(2)
|
||||
if val := opts.RawGetString("log"); val != lua.LNil {
|
||||
logQueries = lua.LVAsBool(val)
|
||||
}
|
||||
}
|
||||
|
||||
conn := &DBConnection{
|
||||
dbPath: dbPath,
|
||||
log: logQueries,
|
||||
logger: llog,
|
||||
writeChan: make(chan *dbWriteRequest, 100),
|
||||
closeChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
go conn.processWrites()
|
||||
|
||||
ud := L.NewUserData()
|
||||
ud.Value = conn
|
||||
L.SetMetatable(ud, L.GetTypeMetatable("gosally_db"))
|
||||
|
||||
L.Push(ud)
|
||||
return 1
|
||||
}))
|
||||
|
||||
mt := L.NewTypeMetatable("gosally_db")
|
||||
L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
|
||||
"exec": dbExec,
|
||||
"query": dbQuery,
|
||||
"query_row": dbQueryRow,
|
||||
"close": dbClose,
|
||||
}))
|
||||
|
||||
L.SetField(dbMod, "__seed", lua.LString(sid))
|
||||
L.Push(dbMod)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *DBConnection) processWrites() {
|
||||
for {
|
||||
select {
|
||||
case req := <-conn.writeChan:
|
||||
mtx := getDBMutex(conn.dbPath)
|
||||
mtx.Lock()
|
||||
|
||||
db, err := sql.Open("sqlite", conn.dbPath+"?_busy_timeout=5000&_journal_mode=WAL&_sync=NORMAL&_cache_size=-10000")
|
||||
if err == nil {
|
||||
_, err = db.Exec("PRAGMA journal_mode=WAL;")
|
||||
if err == nil {
|
||||
res, execErr := db.Exec(req.query, req.args...)
|
||||
if execErr == nil {
|
||||
rows, _ := res.RowsAffected()
|
||||
req.resCh <- &dbWriteResult{rowsAffected: rows}
|
||||
} else {
|
||||
req.resCh <- &dbWriteResult{err: execErr}
|
||||
}
|
||||
}
|
||||
db.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
req.resCh <- &dbWriteResult{err: err}
|
||||
}
|
||||
|
||||
mtx.Unlock()
|
||||
case <-conn.closeChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dbExec(L *lua.LState) int {
|
||||
ud := L.CheckUserData(1)
|
||||
conn, ok := ud.Value.(*DBConnection)
|
||||
if !ok {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString("invalid database connection"))
|
||||
return 2
|
||||
}
|
||||
|
||||
query := L.CheckString(2)
|
||||
|
||||
var args []any
|
||||
if L.GetTop() >= 3 {
|
||||
params := L.CheckTable(3)
|
||||
params.ForEach(func(k lua.LValue, v lua.LValue) {
|
||||
args = append(args, ConvertLuaTypesToGolang(v))
|
||||
})
|
||||
}
|
||||
|
||||
if conn.log {
|
||||
conn.logger.Info("DB Exec",
|
||||
slog.String("query", query),
|
||||
slog.Any("params", args))
|
||||
}
|
||||
|
||||
resCh := make(chan *dbWriteResult, 1)
|
||||
conn.writeChan <- &dbWriteRequest{
|
||||
query: query,
|
||||
args: args,
|
||||
resCh: resCh,
|
||||
}
|
||||
|
||||
ctx := L.NewTable()
|
||||
L.SetField(ctx, "done", lua.LBool(false))
|
||||
|
||||
var result lua.LValue = lua.LNil
|
||||
var errorMsg lua.LValue = lua.LNil
|
||||
|
||||
L.SetField(ctx, "wait", L.NewFunction(func(L *lua.LState) int {
|
||||
res := <-resCh
|
||||
L.SetField(ctx, "done", lua.LBool(true))
|
||||
|
||||
if res.err != nil {
|
||||
errorMsg = lua.LString(res.err.Error())
|
||||
result = lua.LNil
|
||||
} else {
|
||||
result = lua.LNumber(res.rowsAffected)
|
||||
errorMsg = lua.LNil
|
||||
}
|
||||
|
||||
if res.err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(res.err.Error()))
|
||||
return 2
|
||||
}
|
||||
L.Push(lua.LNumber(res.rowsAffected))
|
||||
L.Push(lua.LNil)
|
||||
return 2
|
||||
}))
|
||||
|
||||
L.SetField(ctx, "check", L.NewFunction(func(L *lua.LState) int {
|
||||
select {
|
||||
case res := <-resCh:
|
||||
L.SetField(ctx, "done", lua.LBool(true))
|
||||
if res.err != nil {
|
||||
errorMsg = lua.LString(res.err.Error())
|
||||
result = lua.LNil
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(res.err.Error()))
|
||||
return 2
|
||||
} else {
|
||||
result = lua.LNumber(res.rowsAffected)
|
||||
errorMsg = lua.LNil
|
||||
L.Push(lua.LNumber(res.rowsAffected))
|
||||
L.Push(lua.LNil)
|
||||
return 2
|
||||
}
|
||||
default:
|
||||
L.Push(result)
|
||||
L.Push(errorMsg)
|
||||
return 2
|
||||
}
|
||||
}))
|
||||
|
||||
L.Push(ctx)
|
||||
L.Push(lua.LNil)
|
||||
return 2
|
||||
}
|
||||
|
||||
func dbQueryRow(L *lua.LState) int {
|
||||
ud := L.CheckUserData(1)
|
||||
conn, ok := ud.Value.(*DBConnection)
|
||||
if !ok {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString("invalid database connection"))
|
||||
return 2
|
||||
}
|
||||
|
||||
query := L.CheckString(2)
|
||||
|
||||
var args []any
|
||||
if L.GetTop() >= 3 {
|
||||
params := L.CheckTable(3)
|
||||
params.ForEach(func(k lua.LValue, v lua.LValue) {
|
||||
args = append(args, ConvertLuaTypesToGolang(v))
|
||||
})
|
||||
}
|
||||
|
||||
if conn.log {
|
||||
conn.logger.Info("DB QueryRow",
|
||||
slog.String("query", query),
|
||||
slog.Any("params", args))
|
||||
}
|
||||
|
||||
mtx := getDBMutex(conn.dbPath)
|
||||
mtx.RLock()
|
||||
defer mtx.RUnlock()
|
||||
|
||||
db, err := sql.Open("sqlite", conn.dbPath+"?_busy_timeout=5000&_journal_mode=WAL&_sync=NORMAL&_cache_size=-10000")
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
row := db.QueryRow(query, args...)
|
||||
|
||||
columns := []string{}
|
||||
stmt, err := db.Prepare(query)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("prepare failed: %v", err)))
|
||||
return 2
|
||||
}
|
||||
defer stmt.Close()
|
||||
rows, err := stmt.Query(args...)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("query failed: %v", err)))
|
||||
return 2
|
||||
}
|
||||
defer rows.Close()
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("get columns failed: %v", err)))
|
||||
return 2
|
||||
}
|
||||
for _, c := range cols {
|
||||
columns = append(columns, c)
|
||||
}
|
||||
|
||||
colCount := len(columns)
|
||||
values := make([]any, colCount)
|
||||
valuePtrs := make([]any, colCount)
|
||||
for i := range columns {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
err = row.Scan(valuePtrs...)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
L.Push(lua.LNil)
|
||||
return 1
|
||||
}
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("scan failed: %v", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
rowTable := L.NewTable()
|
||||
for i, col := range columns {
|
||||
val := values[i]
|
||||
if val == nil {
|
||||
L.SetField(rowTable, col, lua.LNil)
|
||||
} else {
|
||||
L.SetField(rowTable, col, ConvertGolangTypesToLua(L, val))
|
||||
}
|
||||
}
|
||||
|
||||
L.Push(rowTable)
|
||||
return 1
|
||||
}
|
||||
|
||||
func dbQuery(L *lua.LState) int {
|
||||
ud := L.CheckUserData(1)
|
||||
conn, ok := ud.Value.(*DBConnection)
|
||||
if !ok {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString("invalid database connection"))
|
||||
return 2
|
||||
}
|
||||
|
||||
query := L.CheckString(2)
|
||||
|
||||
var args []any
|
||||
if L.GetTop() >= 3 {
|
||||
params := L.CheckTable(3)
|
||||
params.ForEach(func(k lua.LValue, v lua.LValue) {
|
||||
args = append(args, ConvertLuaTypesToGolang(v))
|
||||
})
|
||||
}
|
||||
|
||||
if conn.log {
|
||||
conn.logger.Info("DB Query",
|
||||
slog.String("query", query),
|
||||
slog.Any("params", args))
|
||||
}
|
||||
|
||||
mtx := getDBMutex(conn.dbPath)
|
||||
mtx.RLock()
|
||||
defer mtx.RUnlock()
|
||||
|
||||
db, err := sql.Open("sqlite", conn.dbPath+"?_busy_timeout=5000&_journal_mode=WAL&_sync=NORMAL&_cache_size=-10000")
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("query failed: %v", err)))
|
||||
return 2
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("get columns failed: %v", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
result := L.NewTable()
|
||||
colCount := len(columns)
|
||||
values := make([]any, colCount)
|
||||
valuePtrs := make([]any, colCount)
|
||||
|
||||
for rows.Next() {
|
||||
for i := range columns {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("scan failed: %v", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
rowTable := L.NewTable()
|
||||
for i, col := range columns {
|
||||
val := values[i]
|
||||
if val == nil {
|
||||
L.SetField(rowTable, col, lua.LNil)
|
||||
} else {
|
||||
L.SetField(rowTable, col, ConvertGolangTypesToLua(L, val))
|
||||
}
|
||||
}
|
||||
result.Append(rowTable)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("rows iteration failed: %v", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(result)
|
||||
return 1
|
||||
}
|
||||
|
||||
func dbClose(L *lua.LState) int {
|
||||
ud := L.CheckUserData(1)
|
||||
conn, ok := ud.Value.(*DBConnection)
|
||||
if !ok {
|
||||
L.Push(lua.LFalse)
|
||||
L.Push(lua.LString("invalid database connection"))
|
||||
return 2
|
||||
}
|
||||
|
||||
close(conn.closeChan)
|
||||
L.Push(lua.LTrue)
|
||||
return 1
|
||||
}
|
||||
39
internal/server/sv1/handle.go
Normal file
39
internal/server/sv1/handle.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
|
||||
)
|
||||
|
||||
func (h *HandlerV1) Handle(_ context.Context, sid string, r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse {
|
||||
if req.Method == "" {
|
||||
h.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrMethodNotFoundS), slog.String("requested-method", req.Method))
|
||||
return rpc.NewError(rpc.ErrMethodIsMissing, rpc.ErrMethodIsMissingS, nil, req.ID)
|
||||
}
|
||||
|
||||
method, err := h.resolveMethodPath(req.Method)
|
||||
if err != nil {
|
||||
if err.Error() == rpc.ErrInvalidMethodFormatS {
|
||||
h.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrInvalidMethodFormatS), slog.String("requested-method", req.Method))
|
||||
return rpc.NewError(rpc.ErrInvalidMethodFormat, rpc.ErrInvalidMethodFormatS, nil, req.ID)
|
||||
} else if err.Error() == rpc.ErrMethodNotFoundS {
|
||||
h.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrMethodNotFoundS), slog.String("requested-method", req.Method))
|
||||
return rpc.NewError(rpc.ErrMethodNotFound, rpc.ErrMethodNotFoundS, nil, req.ID)
|
||||
}
|
||||
}
|
||||
switch req.Params.(type) {
|
||||
case map[string]any, []any, nil:
|
||||
return h.handleLUA(sid, r, req, method)
|
||||
default:
|
||||
// JSON-RPC 2.0 Specification:
|
||||
// https://www.jsonrpc.org/specification#parameter_structures
|
||||
//
|
||||
// "params" MUST be either an *array* or an *object* if included.
|
||||
// Any other type (e.g., a number, string, or boolean) is INVALID.
|
||||
h.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrInvalidParamsS))
|
||||
return rpc.NewError(rpc.ErrInvalidParams, rpc.ErrInvalidParamsS, nil, req.ID)
|
||||
}
|
||||
}
|
||||
86
internal/server/sv1/jwt.go
Normal file
86
internal/server/sv1/jwt.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func loadJWTMod(llog *slog.Logger, sid string) func(*lua.LState) int {
|
||||
return func(L *lua.LState) int {
|
||||
llog.Debug("import module jwt")
|
||||
jwtMod := L.NewTable()
|
||||
|
||||
L.SetField(jwtMod, "encode", L.NewFunction(jwtEncode))
|
||||
L.SetField(jwtMod, "decode", L.NewFunction(jwtDecode))
|
||||
|
||||
L.SetField(jwtMod, "__seed", lua.LString(sid))
|
||||
L.Push(jwtMod)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func jwtEncode(L *lua.LState) int {
|
||||
payloadTbl := L.CheckTable(1)
|
||||
secret := L.GetField(payloadTbl, "secret").String()
|
||||
payload := L.GetField(payloadTbl, "payload").(*lua.LTable)
|
||||
expiresIn := L.GetField(payloadTbl, "expires_in")
|
||||
expDuration := time.Hour
|
||||
|
||||
if expiresIn.Type() == lua.LTNumber {
|
||||
floatVal := ConvertLuaTypesToGolang(expiresIn).(float64)
|
||||
expDuration = time.Duration(floatVal) * time.Second
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{}
|
||||
payload.ForEach(func(key, value lua.LValue) {
|
||||
claims[key.String()] = ConvertLuaTypesToGolang(value)
|
||||
})
|
||||
claims["iat"] = time.Now().Unix()
|
||||
claims["exp"] = time.Now().Add(expDuration).Unix()
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signedToken, err := token.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(lua.LString(signedToken))
|
||||
return 1
|
||||
}
|
||||
|
||||
func jwtDecode(L *lua.LState) int {
|
||||
tokenString := L.CheckString(1)
|
||||
optsTbl := L.OptTable(2, L.NewTable())
|
||||
secret := L.GetField(optsTbl, "secret").String()
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
L.Push(lua.LString("Invalid token: " + err.Error()))
|
||||
L.Push(lua.LNil)
|
||||
return 2
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
L.Push(lua.LString("Invalid claims"))
|
||||
L.Push(lua.LNil)
|
||||
return 2
|
||||
}
|
||||
|
||||
luaTable := L.NewTable()
|
||||
for k, v := range claims {
|
||||
luaTable.RawSetString(k, ConvertGolangTypesToLua(L, v))
|
||||
}
|
||||
|
||||
L.Push(lua.LNil)
|
||||
L.Push(luaTable)
|
||||
return 2
|
||||
}
|
||||
641
internal/server/sv1/lua_handler.go
Normal file
641
internal/server/sv1/lua_handler.go
Normal file
@@ -0,0 +1,641 @@
|
||||
package sv1
|
||||
|
||||
// TODO: make a lua state pool using sync.Pool
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/colors"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func addInitiatorHeaders(sid string, req *http.Request, headers http.Header) {
|
||||
clientIP := req.RemoteAddr
|
||||
if forwardedFor := req.Header.Get("X-Forwarded-For"); forwardedFor != "" {
|
||||
clientIP = forwardedFor
|
||||
}
|
||||
headers.Set("X-Initiator-IP", clientIP)
|
||||
headers.Set("X-Session-UUID", sid)
|
||||
headers.Set("X-Initiator-Host", req.Host)
|
||||
headers.Set("X-Initiator-User-Agent", req.UserAgent())
|
||||
headers.Set("X-Initiator-Referer", req.Referer())
|
||||
}
|
||||
|
||||
// A small reminder: this code is only at the MVP stage,
|
||||
// and some parts of the code may cause shock from the
|
||||
// incompetence of the developer. But, in the end,
|
||||
// this code is just an idea. If there is a desire to
|
||||
// contribute to the development of the code,
|
||||
// I will be only glad.
|
||||
// TODO: make this huge function more harmonious by dividing responsibilities
|
||||
func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest, path string) *rpc.RPCResponse {
|
||||
var __exit = -1
|
||||
|
||||
llog := h.x.SLog.With(slog.String("session-id", sid))
|
||||
llog.Debug("handling LUA")
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
osMod := L.GetGlobal("os").(*lua.LTable)
|
||||
L.SetField(osMod, "exit", lua.LNil)
|
||||
|
||||
ioMod := L.GetGlobal("io").(*lua.LTable)
|
||||
for _, k := range []string{"write", "output", "flush", "read", "input"} {
|
||||
ioMod.RawSetString(k, lua.LNil)
|
||||
}
|
||||
L.Env.RawSetString("print", lua.LNil)
|
||||
|
||||
for _, name := range []string{"stdout", "stderr", "stdin"} {
|
||||
stream := ioMod.RawGetString(name)
|
||||
if t, ok := stream.(*lua.LUserData); ok {
|
||||
t.Metatable = lua.LNil
|
||||
}
|
||||
}
|
||||
|
||||
seed := rand.Int()
|
||||
|
||||
loadSessionMod := func(L *lua.LState) int {
|
||||
llog.Debug("import module session", slog.String("script", path))
|
||||
sessionMod := L.NewTable()
|
||||
inTable := L.NewTable()
|
||||
paramsTable := L.NewTable()
|
||||
headersTable := L.NewTable()
|
||||
|
||||
fetchedHeadersTable := L.NewTable()
|
||||
for k, v := range r.Header {
|
||||
L.SetField(fetchedHeadersTable, k, ConvertGolangTypesToLua(L, v))
|
||||
}
|
||||
|
||||
headersGetter := L.NewFunction(func(L *lua.LState) int {
|
||||
path := L.OptString(1, "")
|
||||
def := L.Get(2)
|
||||
|
||||
get := func(path string) lua.LValue {
|
||||
if path == "" {
|
||||
return fetchedHeadersTable
|
||||
}
|
||||
fetched := r.Header.Get(path)
|
||||
if fetched == "" {
|
||||
return lua.LNil
|
||||
}
|
||||
return lua.LString(fetched)
|
||||
}
|
||||
val := get(path)
|
||||
if val == lua.LNil && def != lua.LNil {
|
||||
L.Push(def)
|
||||
} else {
|
||||
L.Push(val)
|
||||
}
|
||||
return 1
|
||||
})
|
||||
|
||||
L.SetField(headersTable, "__fetched", fetchedHeadersTable)
|
||||
|
||||
L.SetField(headersTable, "get", headersGetter)
|
||||
L.SetField(inTable, "headers", headersTable)
|
||||
|
||||
fetchedParamsTable := L.NewTable()
|
||||
switch params := req.Params.(type) {
|
||||
case map[string]any:
|
||||
for k, v := range params {
|
||||
L.SetField(fetchedParamsTable, k, ConvertGolangTypesToLua(L, v))
|
||||
}
|
||||
case []any:
|
||||
for i, v := range params {
|
||||
fetchedParamsTable.RawSetInt(i+1, ConvertGolangTypesToLua(L, v))
|
||||
}
|
||||
}
|
||||
|
||||
paramsGetter := L.NewFunction(func(L *lua.LState) int {
|
||||
path := L.OptString(1, "")
|
||||
def := L.Get(2)
|
||||
|
||||
get := func(tbl *lua.LTable, path string) lua.LValue {
|
||||
if path == "" {
|
||||
return tbl
|
||||
}
|
||||
current := tbl
|
||||
parts := strings.Split(path, ".")
|
||||
size := len(parts)
|
||||
for index, key := range parts {
|
||||
val := current.RawGetString(key)
|
||||
if tblVal, ok := val.(*lua.LTable); ok {
|
||||
current = tblVal
|
||||
} else {
|
||||
if index == size-1 {
|
||||
return val
|
||||
}
|
||||
return lua.LNil
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
paramsTbl := L.GetField(paramsTable, "__fetched") //
|
||||
val := get(paramsTbl.(*lua.LTable), path) //
|
||||
if val == lua.LNil && def != lua.LNil {
|
||||
L.Push(def)
|
||||
} else {
|
||||
L.Push(val)
|
||||
}
|
||||
return 1
|
||||
})
|
||||
L.SetField(paramsTable, "__fetched", fetchedParamsTable)
|
||||
|
||||
L.SetField(paramsTable, "get", paramsGetter)
|
||||
L.SetField(inTable, "params", paramsTable)
|
||||
|
||||
outTable := L.NewTable()
|
||||
scriptDataTable := L.NewTable()
|
||||
L.SetField(outTable, "__script_data", scriptDataTable)
|
||||
|
||||
L.SetField(inTable, "address", lua.LString(r.RemoteAddr))
|
||||
|
||||
L.SetField(sessionMod, "throw_error", L.NewFunction(func(L *lua.LState) int {
|
||||
arg := L.Get(1)
|
||||
var msg string
|
||||
switch arg.Type() {
|
||||
case lua.LTString:
|
||||
msg = arg.String()
|
||||
case lua.LTNumber:
|
||||
msg = strconv.FormatFloat(float64(arg.(lua.LNumber)), 'f', -1, 64)
|
||||
default:
|
||||
L.ArgError(1, "expected string or number")
|
||||
return 0
|
||||
}
|
||||
|
||||
L.RaiseError("%s", msg)
|
||||
return 0
|
||||
}))
|
||||
|
||||
resTable := L.NewTable()
|
||||
L.SetField(scriptDataTable, "result", resTable)
|
||||
L.SetField(outTable, "send", L.NewFunction(func(L *lua.LState) int {
|
||||
res := L.Get(1)
|
||||
if res == lua.LNil {
|
||||
__exit = 0
|
||||
L.RaiseError("__successfull")
|
||||
return 0
|
||||
}
|
||||
|
||||
resFTable := scriptDataTable.RawGetString("result")
|
||||
if resPTable, ok := res.(*lua.LTable); ok {
|
||||
resPTable.ForEach(func(key, value lua.LValue) {
|
||||
L.SetField(resFTable, key.String(), value)
|
||||
})
|
||||
} else {
|
||||
L.SetField(scriptDataTable, "result", res)
|
||||
}
|
||||
|
||||
__exit = 0
|
||||
L.RaiseError("__successfull")
|
||||
return 0
|
||||
}))
|
||||
|
||||
L.SetField(outTable, "set", L.NewFunction(func(L *lua.LState) int {
|
||||
res := L.Get(1)
|
||||
if res == lua.LNil {
|
||||
return 0
|
||||
}
|
||||
|
||||
resFTable := scriptDataTable.RawGetString("result")
|
||||
if resPTable, ok := res.(*lua.LTable); ok {
|
||||
resPTable.ForEach(func(key, value lua.LValue) {
|
||||
L.SetField(resFTable, key.String(), value)
|
||||
})
|
||||
} else {
|
||||
L.SetField(scriptDataTable, "result", res)
|
||||
}
|
||||
return 0
|
||||
}))
|
||||
|
||||
errTable := L.NewTable()
|
||||
L.SetField(scriptDataTable, "error", errTable)
|
||||
L.SetField(outTable, "send_error", L.NewFunction(func(L *lua.LState) int {
|
||||
var params [3]lua.LValue
|
||||
for i := range 3 {
|
||||
params[i] = L.Get(i + 1)
|
||||
}
|
||||
if errTable, ok := scriptDataTable.RawGetString("error").(*lua.LTable); ok {
|
||||
for _, v := range params {
|
||||
switch v.Type() {
|
||||
case lua.LTNumber:
|
||||
if n, ok := v.(lua.LNumber); ok {
|
||||
L.SetField(errTable, "code", n)
|
||||
}
|
||||
case lua.LTString:
|
||||
if s, ok := v.(lua.LString); ok {
|
||||
L.SetField(errTable, "message", s)
|
||||
}
|
||||
case lua.LTTable:
|
||||
if tbl, ok := v.(*lua.LTable); ok {
|
||||
L.SetField(errTable, "data", tbl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__exit = 1
|
||||
L.RaiseError("__unsuccessfull")
|
||||
return 0
|
||||
}))
|
||||
|
||||
L.SetField(outTable, "set_error", L.NewFunction(func(L *lua.LState) int {
|
||||
var params [3]lua.LValue
|
||||
for i := range 3 {
|
||||
params[i] = L.Get(i + 1)
|
||||
}
|
||||
if errTable, ok := scriptDataTable.RawGetString("error").(*lua.LTable); ok {
|
||||
for _, v := range params {
|
||||
switch v.Type() {
|
||||
case lua.LTNumber:
|
||||
if n, ok := v.(lua.LNumber); ok {
|
||||
L.SetField(errTable, "code", n)
|
||||
}
|
||||
case lua.LTString:
|
||||
if s, ok := v.(lua.LString); ok {
|
||||
L.SetField(errTable, "message", s)
|
||||
}
|
||||
case lua.LTTable:
|
||||
if tbl, ok := v.(*lua.LTable); ok {
|
||||
L.SetField(errTable, "data", tbl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}))
|
||||
|
||||
L.SetField(sessionMod, "request", inTable)
|
||||
L.SetField(sessionMod, "response", outTable)
|
||||
|
||||
L.SetField(sessionMod, "id", lua.LString(sid))
|
||||
|
||||
L.SetField(sessionMod, "__seed", lua.LString(fmt.Sprint(seed)))
|
||||
L.Push(sessionMod)
|
||||
return 1
|
||||
}
|
||||
|
||||
loadLogMod := func(L *lua.LState) int {
|
||||
llog.Debug("import module log", slog.String("script", path))
|
||||
logMod := L.NewTable()
|
||||
|
||||
logFuncs := map[string]func(string, ...any){
|
||||
"info": llog.Info,
|
||||
"debug": llog.Debug,
|
||||
"error": llog.Error,
|
||||
"warn": llog.Warn,
|
||||
}
|
||||
|
||||
for name, logFunc := range logFuncs {
|
||||
fun := logFunc
|
||||
L.SetField(logMod, name, L.NewFunction(func(L *lua.LState) int {
|
||||
msg := L.Get(1)
|
||||
converted := ConvertLuaTypesToGolang(msg)
|
||||
fun(fmt.Sprintf("the script says: %s", converted), slog.String("script", path))
|
||||
return 0
|
||||
}))
|
||||
}
|
||||
|
||||
for _, fn := range []struct {
|
||||
field string
|
||||
pfunc func(string, ...any)
|
||||
color func() string
|
||||
}{
|
||||
{"event", h.x.Log.Printf, nil},
|
||||
{"event_error", h.x.Log.Printf, colors.PrintError},
|
||||
{"event_warn", h.x.Log.Printf, colors.PrintWarn},
|
||||
} {
|
||||
L.SetField(logMod, fn.field, L.NewFunction(func(L *lua.LState) int {
|
||||
msg := L.Get(1)
|
||||
converted := ConvertLuaTypesToGolang(msg)
|
||||
if fn.color != nil {
|
||||
h.x.Log.Printf("%s: %s: %s", fn.color(), path, converted)
|
||||
} else {
|
||||
h.x.Log.Printf("%s: %s", path, converted)
|
||||
}
|
||||
return 0
|
||||
}))
|
||||
}
|
||||
|
||||
L.SetField(logMod, "__seed", lua.LString(fmt.Sprint(seed)))
|
||||
L.Push(logMod)
|
||||
return 1
|
||||
}
|
||||
|
||||
loadNetMod := func(L *lua.LState) int {
|
||||
llog.Debug("import module net", slog.String("script", path))
|
||||
netMod := L.NewTable()
|
||||
netModhttp := L.NewTable()
|
||||
|
||||
L.SetField(netModhttp, "get_request", L.NewFunction(func(L *lua.LState) int {
|
||||
logRequest := L.ToBool(1)
|
||||
url := L.ToString(2)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
addInitiatorHeaders(sid, r, req.Header)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
if logRequest {
|
||||
llog.Info("HTTP GET request",
|
||||
slog.String("script", path),
|
||||
slog.String("url", url),
|
||||
slog.Int("status", resp.StatusCode),
|
||||
slog.String("status_text", resp.Status),
|
||||
slog.String("initiator_ip", req.Header.Get("X-Initiator-IP")),
|
||||
)
|
||||
}
|
||||
|
||||
result := L.NewTable()
|
||||
L.SetField(result, "status", lua.LNumber(resp.StatusCode))
|
||||
L.SetField(result, "status_text", lua.LString(resp.Status))
|
||||
L.SetField(result, "body", lua.LString(body))
|
||||
L.SetField(result, "content_length", lua.LNumber(resp.ContentLength))
|
||||
|
||||
headers := L.NewTable()
|
||||
for k, v := range resp.Header {
|
||||
L.SetField(headers, k, ConvertGolangTypesToLua(L, v))
|
||||
}
|
||||
L.SetField(result, "headers", headers)
|
||||
|
||||
L.Push(result)
|
||||
return 1
|
||||
}))
|
||||
|
||||
L.SetField(netModhttp, "post_request", L.NewFunction(func(L *lua.LState) int {
|
||||
logRequest := L.ToBool(1)
|
||||
url := L.ToString(2)
|
||||
contentType := L.ToString(3)
|
||||
payload := L.ToString(4)
|
||||
|
||||
body := strings.NewReader(payload)
|
||||
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
|
||||
addInitiatorHeaders(sid, r, req.Header)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
if logRequest {
|
||||
llog.Info("HTTP POST request",
|
||||
slog.String("script", path),
|
||||
slog.String("url", url),
|
||||
slog.String("content_type", contentType),
|
||||
slog.Int("status", resp.StatusCode),
|
||||
slog.String("status_text", resp.Status),
|
||||
slog.String("initiator_ip", req.Header.Get("X-Initiator-IP")),
|
||||
)
|
||||
}
|
||||
|
||||
result := L.NewTable()
|
||||
L.SetField(result, "status", lua.LNumber(resp.StatusCode))
|
||||
L.SetField(result, "status_text", lua.LString(resp.Status))
|
||||
L.SetField(result, "body", lua.LString(respBody))
|
||||
L.SetField(result, "content_length", lua.LNumber(resp.ContentLength))
|
||||
|
||||
headers := L.NewTable()
|
||||
for k, v := range resp.Header {
|
||||
L.SetField(headers, k, ConvertGolangTypesToLua(L, v))
|
||||
}
|
||||
L.SetField(result, "headers", headers)
|
||||
|
||||
L.Push(result)
|
||||
return 1
|
||||
}))
|
||||
|
||||
L.SetField(netMod, "http", netModhttp)
|
||||
|
||||
L.SetField(netMod, "__seed", lua.LString(fmt.Sprint(seed)))
|
||||
L.Push(netMod)
|
||||
return 1
|
||||
}
|
||||
|
||||
loadCryptbcryptMod := func(L *lua.LState) int {
|
||||
llog.Debug("import module crypt.bcrypt", slog.String("script", path))
|
||||
bcryptMod := L.NewTable()
|
||||
|
||||
L.SetField(bcryptMod, "MinCost", lua.LNumber(bcrypt.MinCost))
|
||||
L.SetField(bcryptMod, "MaxCost", lua.LNumber(bcrypt.MaxCost))
|
||||
L.SetField(bcryptMod, "DefaultCost", lua.LNumber(bcrypt.DefaultCost))
|
||||
|
||||
L.SetField(bcryptMod, "generate", L.NewFunction(func(l *lua.LState) int {
|
||||
password := ConvertLuaTypesToGolang(L.Get(1))
|
||||
passwordStr, ok := password.(string)
|
||||
if !ok {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString("error: password must be a string"))
|
||||
return 2
|
||||
}
|
||||
|
||||
cost := ConvertLuaTypesToGolang(L.Get(2))
|
||||
costInt := bcrypt.DefaultCost
|
||||
switch v := cost.(type) {
|
||||
case int:
|
||||
costInt = v
|
||||
case float64:
|
||||
costInt = int(v)
|
||||
case nil:
|
||||
// ok, use DefaultCost
|
||||
default:
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString("error: cost must be an integer"))
|
||||
return 2
|
||||
}
|
||||
|
||||
hashBytes, err := bcrypt.GenerateFromPassword([]byte(passwordStr), costInt)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString("error: " + err.Error()))
|
||||
return 2
|
||||
}
|
||||
|
||||
L.Push(lua.LString(string(hashBytes)))
|
||||
L.Push(lua.LNil)
|
||||
return 2
|
||||
}))
|
||||
|
||||
L.SetField(bcryptMod, "compare", L.NewFunction(func(l *lua.LState) int {
|
||||
hash := ConvertLuaTypesToGolang(L.Get(1))
|
||||
hashStr, ok := hash.(string)
|
||||
if !ok {
|
||||
L.Push(lua.LString("error: hash must be a string"))
|
||||
return 1
|
||||
}
|
||||
password := ConvertLuaTypesToGolang(L.Get(2))
|
||||
passwordStr, ok := password.(string)
|
||||
if !ok {
|
||||
L.Push(lua.LString("error: password must be a string"))
|
||||
return 1
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashStr), []byte(passwordStr))
|
||||
if err != nil {
|
||||
L.Push(lua.LFalse)
|
||||
return 1
|
||||
}
|
||||
L.Push(lua.LTrue)
|
||||
return 1
|
||||
}))
|
||||
|
||||
L.SetField(bcryptMod, "__seed", lua.LString(fmt.Sprint(seed)))
|
||||
L.Push(bcryptMod)
|
||||
return 1
|
||||
}
|
||||
|
||||
loadCryptbsha256Mod := func(L *lua.LState) int {
|
||||
llog.Debug("import module crypt.sha256", slog.String("script", path))
|
||||
sha265mod := L.NewTable()
|
||||
|
||||
L.SetField(sha265mod, "sum", L.NewFunction(func(l *lua.LState) int {
|
||||
data := ConvertLuaTypesToGolang(L.Get(1))
|
||||
var dataStr = fmt.Sprint(data)
|
||||
|
||||
hash := sha256.Sum256([]byte(dataStr))
|
||||
|
||||
L.Push(lua.LString(hex.EncodeToString(hash[:])))
|
||||
L.Push(lua.LNil)
|
||||
return 2
|
||||
}))
|
||||
|
||||
L.SetField(sha265mod, "__seed", lua.LString(fmt.Sprint(seed)))
|
||||
L.Push(sha265mod)
|
||||
return 1
|
||||
}
|
||||
|
||||
L.PreloadModule("internal.session", loadSessionMod)
|
||||
L.PreloadModule("internal.log", loadLogMod)
|
||||
L.PreloadModule("internal.net", loadNetMod)
|
||||
L.PreloadModule("internal.database.sqlite", loadDBMod(llog, fmt.Sprint(seed)))
|
||||
L.PreloadModule("internal.crypt.bcrypt", loadCryptbcryptMod)
|
||||
L.PreloadModule("internal.crypt.sha256", loadCryptbsha256Mod)
|
||||
L.PreloadModule("internal.crypt.jwt", loadJWTMod(llog, fmt.Sprint(seed)))
|
||||
|
||||
llog.Debug("preparing environment")
|
||||
prep := filepath.Join(*h.x.Config.Conf.Node.ComDir, "_prepare.lua")
|
||||
if _, err := os.Stat(prep); err == nil {
|
||||
if err := L.DoFile(prep); err != nil {
|
||||
llog.Error("script error", slog.String("script", path), slog.String("error", err.Error()))
|
||||
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
|
||||
}
|
||||
}
|
||||
llog.Debug("executing script", slog.String("script", path))
|
||||
err := L.DoFile(path)
|
||||
if err != nil && __exit != 0 && __exit != 1 {
|
||||
llog.Error("script error", slog.String("script", path), slog.String("error", err.Error()))
|
||||
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
|
||||
}
|
||||
|
||||
pkg := L.GetGlobal("package")
|
||||
pkgTbl, ok := pkg.(*lua.LTable)
|
||||
if !ok {
|
||||
llog.Error("script error", slog.String("script", path), slog.String("error", "package not found"))
|
||||
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
|
||||
}
|
||||
|
||||
loaded := pkgTbl.RawGetString("loaded")
|
||||
loadedTbl, ok := loaded.(*lua.LTable)
|
||||
if !ok {
|
||||
llog.Error("script error", slog.String("script", path), slog.String("error", "package.loaded not found"))
|
||||
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
|
||||
}
|
||||
|
||||
sessionVal := loadedTbl.RawGetString("internal.session")
|
||||
sessionTbl, ok := sessionVal.(*lua.LTable)
|
||||
if !ok {
|
||||
return rpc.NewResponse(nil, req.ID)
|
||||
}
|
||||
|
||||
tag := sessionTbl.RawGetString("__seed")
|
||||
if tag.Type() != lua.LTString || tag.String() != fmt.Sprint(seed) {
|
||||
llog.Debug("stock session module is not imported: wrong seed", slog.String("script", path))
|
||||
return rpc.NewResponse(nil, req.ID)
|
||||
}
|
||||
|
||||
outVal := sessionTbl.RawGetString("response")
|
||||
outTbl, ok := outVal.(*lua.LTable)
|
||||
if !ok {
|
||||
llog.Error("script error", slog.String("script", path), slog.String("error", "response is not a table"))
|
||||
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
|
||||
}
|
||||
|
||||
if scriptDataTable, ok := outTbl.RawGetString("__script_data").(*lua.LTable); ok {
|
||||
switch __exit {
|
||||
case 1:
|
||||
if errTbl, ok := scriptDataTable.RawGetString("error").(*lua.LTable); ok {
|
||||
llog.Debug("catch error table", slog.String("script", path))
|
||||
code := rpc.ErrInternalError
|
||||
message := rpc.ErrInternalErrorS
|
||||
if c := errTbl.RawGetString("code"); c.Type() == lua.LTNumber {
|
||||
code = int(c.(lua.LNumber))
|
||||
}
|
||||
if msg := errTbl.RawGetString("message"); msg.Type() == lua.LTString {
|
||||
message = msg.String()
|
||||
}
|
||||
data := ConvertLuaTypesToGolang(errTbl.RawGetString("data"))
|
||||
llog.Error("the script terminated with an error", slog.Int("code", code), slog.String("message", message), slog.Any("data", data))
|
||||
return rpc.NewError(code, message, data, req.ID)
|
||||
}
|
||||
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
|
||||
case 0:
|
||||
resVal := ConvertLuaTypesToGolang(scriptDataTable.RawGetString("result"))
|
||||
return rpc.NewResponse(resVal, req.ID)
|
||||
}
|
||||
}
|
||||
return rpc.NewResponse(nil, req.ID)
|
||||
}
|
||||
126
internal/server/sv1/lua_types.go
Normal file
126
internal/server/sv1/lua_types.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func ConvertLuaTypesToGolang(value lua.LValue) any {
|
||||
switch value.Type() {
|
||||
case lua.LTString:
|
||||
return value.String()
|
||||
case lua.LTNumber:
|
||||
return float64(value.(lua.LNumber))
|
||||
case lua.LTBool:
|
||||
return bool(value.(lua.LBool))
|
||||
case lua.LTTable:
|
||||
tbl := value.(*lua.LTable)
|
||||
|
||||
maxIdx := 0
|
||||
isArray := true
|
||||
|
||||
var isNumeric = false
|
||||
tbl.ForEach(func(key, _ lua.LValue) {
|
||||
var numKey lua.LValue
|
||||
var ok bool
|
||||
switch key.Type() {
|
||||
case lua.LTString:
|
||||
numKey, ok = key.(lua.LString)
|
||||
if !ok {
|
||||
isArray = false
|
||||
return
|
||||
}
|
||||
case lua.LTNumber:
|
||||
numKey, ok = key.(lua.LNumber)
|
||||
if !ok {
|
||||
isArray = false
|
||||
return
|
||||
}
|
||||
isNumeric = true
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(numKey.String())
|
||||
if err != nil {
|
||||
isArray = false
|
||||
return
|
||||
}
|
||||
if num < 1 {
|
||||
isArray = false
|
||||
return
|
||||
}
|
||||
if num > maxIdx {
|
||||
maxIdx = num
|
||||
}
|
||||
})
|
||||
|
||||
if isArray {
|
||||
arr := make([]any, maxIdx)
|
||||
if isNumeric {
|
||||
for i := 1; i <= maxIdx; i++ {
|
||||
arr[i-1] = ConvertLuaTypesToGolang(tbl.RawGetInt(i))
|
||||
}
|
||||
} else {
|
||||
for i := 1; i <= maxIdx; i++ {
|
||||
arr[i-1] = ConvertLuaTypesToGolang(tbl.RawGetString(strconv.Itoa(i)))
|
||||
}
|
||||
}
|
||||
return arr
|
||||
}
|
||||
result := make(map[string]any)
|
||||
tbl.ForEach(func(key, val lua.LValue) {
|
||||
result[key.String()] = ConvertLuaTypesToGolang(val)
|
||||
})
|
||||
return result
|
||||
|
||||
case lua.LTNil:
|
||||
return nil
|
||||
default:
|
||||
return value.String()
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertGolangTypesToLua(L *lua.LState, val any) lua.LValue {
|
||||
if val == nil {
|
||||
return lua.LNil
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(val)
|
||||
rt := rv.Type()
|
||||
|
||||
switch rt.Kind() {
|
||||
case reflect.String:
|
||||
return lua.LString(rv.String())
|
||||
case reflect.Bool:
|
||||
return lua.LBool(rv.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return lua.LNumber(rv.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return lua.LNumber(rv.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return lua.LNumber(rv.Float())
|
||||
|
||||
case reflect.Slice, reflect.Array:
|
||||
tbl := L.NewTable()
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
tbl.RawSetInt(i+1, ConvertGolangTypesToLua(L, rv.Index(i).Interface()))
|
||||
}
|
||||
return tbl
|
||||
|
||||
case reflect.Map:
|
||||
if rt.Key().Kind() == reflect.String {
|
||||
tbl := L.NewTable()
|
||||
for _, key := range rv.MapKeys() {
|
||||
val := rv.MapIndex(key)
|
||||
tbl.RawSetString(key.String(), ConvertGolangTypesToLua(L, val.Interface()))
|
||||
}
|
||||
return tbl
|
||||
}
|
||||
|
||||
default:
|
||||
return lua.LString(fmt.Sprintf("%v", val))
|
||||
}
|
||||
return lua.LString(fmt.Sprintf("%v", val))
|
||||
}
|
||||
26
internal/server/sv1/path.go
Normal file
26
internal/server/sv1/path.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
|
||||
)
|
||||
|
||||
func (h *HandlerV1) resolveMethodPath(method string) (string, error) {
|
||||
if !h.allowedCmd.MatchString(method) {
|
||||
return "", errors.New(rpc.ErrInvalidMethodFormatS)
|
||||
}
|
||||
|
||||
parts := strings.Split(method, ">")
|
||||
relPath := filepath.Join(parts...) + ".lua"
|
||||
fullPath := filepath.Join(*h.x.Config.Conf.Node.ComDir, relPath)
|
||||
|
||||
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
||||
return "", errors.New(rpc.ErrMethodNotFoundS)
|
||||
}
|
||||
|
||||
return fullPath, nil
|
||||
}
|
||||
@@ -3,30 +3,27 @@
|
||||
package sv1
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"regexp"
|
||||
|
||||
"github.com/akyaiy/GoSally-mvp/core/config"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
|
||||
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
|
||||
)
|
||||
|
||||
// HandlerV1InitStruct structure is only for initialization
|
||||
type HandlerV1InitStruct struct {
|
||||
Ver string
|
||||
Log slog.Logger
|
||||
Config *config.Conf
|
||||
AllowedCmd *regexp.Regexp
|
||||
ListAllowedCmd *regexp.Regexp
|
||||
Ver string
|
||||
CS *corestate.CoreState
|
||||
X *app.AppX
|
||||
AllowedCmd *regexp.Regexp
|
||||
}
|
||||
|
||||
// HandlerV1 implements the ServerV1UtilsContract and serves as the main handler for API requests.
|
||||
type HandlerV1 struct {
|
||||
log *slog.Logger
|
||||
|
||||
cfg *config.Conf
|
||||
cs *corestate.CoreState
|
||||
x *app.AppX
|
||||
|
||||
// allowedCmd and listAllowedCmd are regular expressions used to validate command names.
|
||||
allowedCmd *regexp.Regexp
|
||||
listAllowedCmd *regexp.Regexp
|
||||
allowedCmd *regexp.Regexp
|
||||
|
||||
ver string
|
||||
}
|
||||
@@ -36,11 +33,10 @@ type HandlerV1 struct {
|
||||
// because there is no validation of parameters in this function.
|
||||
func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 {
|
||||
return &HandlerV1{
|
||||
log: &o.Log,
|
||||
cfg: o.Config,
|
||||
allowedCmd: o.AllowedCmd,
|
||||
listAllowedCmd: o.ListAllowedCmd,
|
||||
ver: o.Ver,
|
||||
cs: o.CS,
|
||||
x: o.X,
|
||||
allowedCmd: o.AllowedCmd,
|
||||
ver: o.Ver,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user