Compare commits

91 Commits

Author SHA1 Message Date
d4413c433f fmt 2025-11-02 11:04:12 +02:00
d9a4bb7871 fix fmt 2025-11-02 10:17:49 +02:00
8e017af3ed change sv1 version 2025-11-01 10:22:14 +02:00
ef6023330d 2025-10-22 15:05:12 +03:00
5474b22fc8 Update README.md 2025-10-12 13:11:55 +03:00
6cd678d9f1 Update README.md 2025-10-12 13:10:10 +03:00
856d3b418c Update README.md 2025-10-12 13:06:37 +03:00
5734ca7a67 update readme.md 2025-10-12 12:56:30 +03:00
608c5aed4a Update README.md 2025-10-12 12:46:07 +03:00
Aleksey
d4d04115f3 Update README.md 2025-10-12 11:10:21 +03:00
Aleksey
4b916f4fc9 Update README.md 2025-10-12 10:46:41 +03:00
54cc496c39 fix noname node message 2025-10-12 01:57:52 +03:00
f7b0014a37 fix bug 2025-10-12 01:56:11 +03:00
54eb5eec6a add wiki to gitignore 2025-10-11 23:20:14 +03:00
6cc24a1e7f update readme 2025-10-11 21:07:51 +03:00
ea41c435dd update readme 2025-10-11 21:04:59 +03:00
d24e1a94ae 2025-10-11 21:01:17 +03:00
846dc06601 update makefile 2025-10-11 16:31:35 +03:00
740fbbff78 fix version message 2025-10-11 16:25:17 +03:00
40be3c8d09 add sv1 version 2025-10-11 16:24:58 +03:00
9c140abc6d make a table 2025-10-11 12:49:53 +03:00
e90233aec4 complete part of readme 2025-10-11 12:44:48 +03:00
df1ef57769 fix bugs 2025-10-10 22:58:31 +03:00
4c840c40bb some changes 2025-10-10 22:54:29 +03:00
57f35e8f33 move go files to src/ 2025-10-10 22:46:24 +03:00
f0c591f325 delete old files 2025-10-10 22:28:55 +03:00
36ee320c45 comment upx 2025-10-10 22:22:34 +03:00
ee6fd205d5 fix the use of empty fields in the response 2025-10-10 22:22:15 +03:00
bed0471cc4 remove some messages 2025-10-10 22:03:21 +03:00
e3812a18a6 require bcrypt only if needed 2025-10-10 20:35:19 +03:00
b7d939d5d7 optimise db exec 2025-10-10 20:23:24 +03:00
c737e80b8f add password changing support 2025-10-10 20:02:17 +03:00
5783a756c3 add method update 2025-10-10 19:27:04 +03:00
ba47ee4219 move error messages to variables 2025-10-10 19:26:57 +03:00
5d49e0afc7 add _errors.lua 2025-10-10 19:26:35 +03:00
76fed578ff create fully functional get method 2025-10-09 20:26:25 +03:00
975c52b58e delete unused modules 2025-10-09 20:03:44 +03:00
4e75d48f1d small fixes 2025-10-09 20:00:48 +03:00
65af07fffa add delete method 2025-10-09 20:00:40 +03:00
1252634420 Change method separator to . and move separator symbol and regexp template to global variables 2025-10-09 19:56:41 +03:00
4a58845211 deledet some files 2025-10-05 19:10:53 +03:00
b0701632e6 add common function to Unit layer 2025-10-05 19:10:45 +03:00
9277aa9f1a add some files to ginignore 2025-10-05 19:10:31 +03:00
19654e1eca Ad some CRUD methods to manage units table 2025-10-05 19:09:58 +03:00
d4306a0d89 rename internal.sha256.sum to hash 2025-10-05 19:09:00 +03:00
73095a69e0 Merge branch 'main' of https://github.com/akyaiy/GoSally-mvp 2025-09-12 19:19:08 +03:00
0f82ce941b in 2025-09-12 19:18:22 +03:00
Aleksey
0ec8493ab4 Merge pull request #3 from akyaiy/auth-server
Auth server
2025-09-12 19:16:32 +03:00
625e5daf71 commit to merge branches 2025-09-12 19:13:46 +03:00
cc27843bb3 add GetSoneInfo (not functional) 2025-08-20 10:12:44 +03:00
20fec82159 delete old scripts 2025-08-20 10:12:15 +03:00
055b299ecb some changes with scripts and add new 2025-08-20 10:12:02 +03:00
17bf207087 fix little bug with script ending 2025-08-20 10:11:36 +03:00
7ae8e12dc8 add new method 2025-08-20 10:11:14 +03:00
6e36db428a fmt 2025-08-20 10:11:02 +03:00
06103a3264 add lua engine skeleton 2025-08-20 10:10:52 +03:00
c6da55ad65 some changes with data field, and fix smth 2025-08-10 16:26:41 +03:00
20a1e3e7bb fix the List.lua 2025-08-10 16:26:21 +03:00
e594d519a7 add set and set_error methods and fix some bugs 2025-08-10 09:49:35 +03:00
2ceb236a53 some small changes, and add send, send_error, throw_error and some field 2025-08-09 10:41:50 +03:00
811403a0a2 echo test function 2025-08-09 10:41:13 +03:00
b451f2d3fc fix jwt dep 2025-08-09 10:41:01 +03:00
5c01eaad6f rename field __gosally_internal to __seed 2025-08-07 19:58:42 +03:00
2b38e179db remove a mistake from specification 2025-08-07 15:46:27 +03:00
2889092821 fix some bugs with params and add params type check 2025-08-07 15:43:49 +03:00
3df3a7b4b5 remove default case because it's not allowed 2025-08-06 22:24:13 +03:00
c63f1bd123 remove default case because it's not allowed 2025-08-06 22:23:58 +03:00
095b8559f4 fix bug with params's array.. again 2025-08-06 19:48:02 +03:00
39532f22ea fix bug with result array 2025-08-06 19:36:08 +03:00
35cebee819 fix bug with empty result and non table result 2025-08-06 19:17:10 +03:00
84dfdd6b35 add sha356 module 2025-08-06 16:37:28 +03:00
e693efe8e7 add iat to jwt 2025-08-06 16:37:13 +03:00
c3dcf24e50 improve jwt 2025-08-06 16:36:55 +03:00
9e7d99e854 fmt 2025-08-06 16:36:39 +03:00
7f2783b39a add postfix -md5 to checksum field 2025-08-06 15:45:20 +03:00
c08135309f add random salt and result/error checksum 2025-08-06 15:41:25 +03:00
cd9e3ab6c4 remove phone_number from db query 2025-08-06 15:40:42 +03:00
adaedf195f solve a little problem with array's fetching 2025-08-06 14:35:17 +03:00
87694f6654 make ConvertGolangTypesToLua simplier 2025-08-06 14:01:45 +03:00
fe628e0f7f develop jwt auth for methods 2025-08-06 14:01:27 +03:00
3898e2833b fmt 2025-08-06 11:32:04 +03:00
e4db8505a0 add sid 2025-08-06 11:31:42 +03:00
0c25d00171 add github.com/golang-jwt/jwt/v5 to the project 2025-08-06 11:31:33 +03:00
b5a6de0b62 add jwt support 2025-08-06 11:31:14 +03:00
1d3d74846e rename database-sqlite to database.sqlite 2025-08-06 10:42:26 +03:00
0141427bfe add print to not allowed functions 2025-08-06 10:04:22 +03:00
866946646b delete some io.* writing functions 2025-08-06 09:58:40 +03:00
Aleksey
24eef9eee0 Merge pull request #2 from akyaiy/dev
Dev
2025-08-04 15:15:00 +03:00
Aleksey
a6c9e5102f Merge branch 'main' into dev 2025-08-04 15:13:55 +03:00
f3c4b9e9b1 update config example 2025-07-30 12:02:12 +03:00
Aleksey
81359c036c Merge pull request #1 from akyaiy/dev
Dev
2025-07-29 16:51:18 +03:00
73 changed files with 1397 additions and 807 deletions

8
.gitignore vendored
View File

@@ -10,4 +10,10 @@ com/_config.lua
.vscode
Taskfile.yml
config.yaml
config.yaml
wiki
# Garbage
com/_*
com/test.lua

View File

@@ -4,7 +4,10 @@ GOPATH := $(shell go env GOPATH)
export CONFIG_PATH := ./config.yaml
export NODE_PATH := $(shell pwd)
LDFLAGS := -X 'github.com/akyaiy/GoSally-mvp/internal/engine/config.NodeVersion=v0.0.1-dev'
NODE_VERSION := v0.0.1-dev
SV1_VERSION := v0.0.1-dev
LDFLAGS := -X 'github.com/akyaiy/GoSally-mvp/src/internal/engine/config.NodeVersion=$(NODE_VERSION)' -X 'github.com/akyaiy/GoSally-mvp/src/internal/server/sv1.SV1Version=$(SV1_VERSION)'
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
@@ -29,15 +32,15 @@ build:
@echo "Building..."
@# @echo "CGO_CFLAGS is: '$(CGO_CFLAGS)'"
@# @echo "CGO_LDFLAGS is: '$(CGO_LDFLAGS)'"
@# CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)"
@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
@# CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)"
cd src && 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!"
@@ -52,30 +55,22 @@ pure-run:
exec ./$(BIN_DIR)/$(APP_NAME)
test:
@go test ./... | grep -v '^?' || true
@cd src && go test ./... | grep -v '^?' || true
fmt:
@go fmt ./internal/./...
@go fmt ./cmd/./...
@go fmt ./hooks/./...
@$(GOPATH)/bin/goimports -w ./internal/
@$(GOPATH)/bin/goimports -w ./cmd/
@$(GOPATH)/bin/goimports -w ./hooks/
@cd src && go fmt .
@cd src && $(GOPATH)/bin/goimports -w .
vet:
@go vet ./...
lint:
@$(GOPATH)/bin/golangci-lint run
@cd src && go vet ./...
check: fmt vet lint test
licenses:
lint:
@cd src && $(GOPATH)/bin/golangci-lint run ./...
@$(GOPATH)/bin/go-licenses save ./... --save_path=third_party/licenses --force
@echo "Licenses have been exported to third_party/licenses"
clean:
@rm -rf bin
licenses:
@cd src && $(GOPATH)/bin/go-licenses save ./... --save_path=../third_party/licenses --force
@echo "Licenses have been exported to third_party/licenses"
help:
@echo "Available commands: $$(cat Makefile | grep -E '^[a-zA-Z_-]+:.*?' | grep -v -- '-setup:' | sed 's/:.*//g' | sort | uniq | tr '\n' ' ')"

147
README.md
View File

@@ -1,51 +1,132 @@
# Go Sally MVP (Minimum/Minimal Viable Product)
[![Status](https://img.shields.io/badge/status-MVP-orange.svg)]()
[![Go Version](https://img.shields.io/badge/Go-1.24.6-informational)](https://go.dev/)
[![Lua Version](https://img.shields.io/badge/Lua-5.1-informational)](https://www.lua.org/manual/5.1/)
[![Go Reference](https://pkg.go.dev/badge/github.com/akyaiy/GoSally-mvp.svg)](https://pkg.go.dev/github.com/akyaiy/GoSally-mvp)
[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue)](LICENSE)
### What is this?
System that allows you to build your own infrastructure based on identical nodes and various scripts written using built-in Lua 5.1, shebang scripts (scripts that start with the `#!` symbols), compiled binaries.
[![Last Commit](https://img.shields.io/github/last-commit/akyaiy/GoSally-mvp)]()
[![Commits per month](https://img.shields.io/github/commit-activity/m/akyaiy/GoSally-mvp)]()
[![Docs](https://img.shields.io/badge/docs-wiki-blue)](https://github.com/akyaiy/GoSally-mvp/wiki)
### Features
Go Sally is not viable at the moment, but it already has the ability to run embedded scripts, log slog events to stdout, handle RPC like requests, and independent automatic update from the repository (my pride, to be honest).
### Example of use
The basic directory tree looks something like this
```
.
├── bin
│   └── node Node core binary file
├── com
│   ├── echo.lua
│   ├── _globals.lua Declaring global variables and functions for all internal scripts (also required for luarc to work correctly)
│   └── _prepare.lua Script that is executed before each script launch
└── config.yaml
> ⚡ **What, Why, Why Care?**
3 directories, 5 files
> **What:** Go Sally is a lightweight decentralized node system with Lua scripting and JSON-RPC2.0.
```
Launch by command
> **Why:** Large admin tools are too heavy, and Raspberry Pi and small servers require a lightweight, modular architecture.
> **Why Care:** Create, automate, and expand your infrastructure quickly, without unnecessary software or dependencies.
## Navigation
* [Core features](#core-features)
* [Quick start](#quick-start)
* [Test it](#test-it)
* [Concept](#concept)
* [API](#api)
* [License](#license)
* [Wiki →](https://github.com/akyaiy/GoSally-mvp/wiki)
> [!NOTE]
> If you see "💡" in the text, it means the information below is about plans for the future of the project.
## Core features
- **Decentralized nodes**<details>this means that *multiple GS[^1] nodes can be located on a single machine*, provided no attempt is made to disrupt, sabotage, or bypass the built-in protection mechanism against running a node under the same identifier as one already running in the system. Identification plays a role in node communication. 💡 In the future, we plan to create tools for conveniently building distributed systems using node identification.
**Why Care?** Multiple nodes on one machine allow testing, experimentation, and scaling small infrastructures without extra hardware or complex setup.</details>
- **RPC request processing**<details>the GS operates *using HTTP/https and the JSONRPC2.0 protocol.* Unlike gRPC, jsonrpc is extremely simple, allows for easy sending of requests from the browser, and does not require any additional code compilation. **Why Care?** Easy-to-use API means you can control nodes from anywhere, including lightweight web clients, without compiling extra code.</details>
- **Lua script-based methods**<details>*The gopher-lua library is used, providing full support for Lua 5.1.* scripts implement libraries for interacting with sessions (receiving parameters and sending responses), hashing, logging, and more. This allows you to quickly write business logic on the fly without touching the lower layers of abstraction, which also eliminates unnecessary compilation and the risk of breaking the codebase.
Example of the "echo" method:
```lua
local session = require("internal.session")
-- import the internal library for interacting with sessions
session.response.send(session.request.params.get())
-- send everything passed in the parameters in response.
```
**Why Care?** You can extend node behavior dynamically, write custom logic fast, and iterate without recompiling — perfect for experiments or automation.
</details>
- **Relatively flexible configuration** <details>
you can configure the server port, address, name, node settings, and more. 💡 More settings are planned in the future. **Why Care?** Configure nodes for any environment, from Raspberry Pi to VPS, without touching the source code. obviously :)</details>
- ***And more in the future***
> [!IMPORTANT]
> This is the beginning of the project's development, and some aspects of it may be unstable, unfinished, and the text about it may be overly ambitious. It's just a matter of time.
## Quick start
```bash
git clone https://github.com/akyaiy/GoSally-mvp.git && \
cd GoSally-mvp && \
make build && \
echo -e "node:\n com_dir: \"%path%/com\"" > config.yaml && \
mkdir -p com && \
echo -e 'local session = require("internal.session")\n\nsession.response.send(session.request.params.get())' > com/echo.lua && \
./bin/node run
```
or for structured logs
```bash
./bin/node run | jq
```
Example of GET request to server
If you have problems, make sure you have all [dependencies](https://github.com/akyaiy/GoSally-mvp/wiki/Getting-started#installing-dependencies) installed, otherwise [file an issue report](https://github.com/akyaiy/GoSally-mvp/issues)
### Test it
```bash
curl -s http://localhost:8080/api/v1/com/echo?msg=Hello
curl -X POST http://localhost:8080/com \
-d '{"jsonrpc":"2.0","context-version": "v1","method":"echo","params":["Hi!!"],"id":1}'
```
Then the response from the server
Expected response:
```json
{
"ResponsibleAgentUUID": "4593a87000bbe088f4e79c477e9c90d3",
"RequestedCommand": "echo",
"Response": {
"answer": "Hello",
"status": "ok"
"jsonrpc": "2.0",
"id": 1,
"result": [
"Hi!!"
],
"data": {
"responsible-node": "a0e1c440473ffd4d87e32cff2717f5b3",
"salt": "f26df732-a3be-4400-8e71-b8dc3ba705fc",
"checksum-md5": "cd8bec6a365d1b8ee90773567cb3ad0a"
}
}
```
### How to install
**You don't need it now, but you can figure it out with the Makefile**
## Concept
The project was originally conceived as a tool for building infrastructure using relatively *small nodes with limited functionality*. 💡 In the future, we plan to create a *web interface for interacting with nodes, administration, and configuration*. The concept is simple: suppose we have a node that manages Bind9. It has all the necessary methods for interacting with the service: creating new zones, viewing zone status, changing configuration, and server operation status. All of this works only through manual configuration, with the exception of larger solutions like Webmin and the BIND DNS Server module. The big problem is that while we only needed web configuration for Bind9, we have to pull in a massive amount of software just to implement one module. What if the service is hosted on a low-power Raspberry Pi? That's where GS nodes come in. By default, GS nodes communicate only through API calls, so 💡 in the future, we plan to create a dedicated, also programmable, web node that will provide convenient access to node management.
There's an obvious advantage here: transparency. The project is *completely open source and aims to support community-driven node functionality*. 💡 In the future, we plan to create a "store" similar to Docker Hub, which will contain scripts for configuring bind9, openvpn, and even custom projects.
## API
As mentioned earlier, *the server handles [jsonrpc2.0](https://www.jsonrpc.org/specification) requests*
```json
{
"jsonrpc": "2.0",
"context-version": "v1",
"method": "test",
"params": [
"Hi!!"
],
"id": 1
}
```
This is a typical example of a request using the jsonrpc2.0 protocol.
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": [
"Hi!!"
],
"data": {
"responsible-node": "2ad6ebeaf579a7c52801fb6c9dd1b83d",
"salt": "e7a81115-01c1-45b1-9618-0eae0ff26451",
"checksum-md5": "cd8bec6a365d1b8ee90773567cb3ad0a"
}
}
```
In the result field, we see the echo method's response. Those familiar with the jsonrpc2.0 specification will notice that the data structure here is unclear. This is my extension, which has three functions:
| Field | Type | Description |
|--------|------|-------------|
| `responsible-node` | string | ID of the node that executed the task |
| `salt` | string | Random value for each request — can be used to check that the response is unique |
| `checksum-md5` | string | MD5 hash of the result field — can be used to avoid processing identical results separately |
## License
Distributed under the BSD 3-Clause License. See [`LICENSE`](./LICENSE) for more information.
[^1]: Go Sally

22
com/Access/_common.lua Normal file
View File

@@ -0,0 +1,22 @@
-- File com/Access/_common.lua
--
-- Created at 2025-21-10
--
-- Description:
--- Common functions for Unit module
local common = {}
function common.CheckMissingElement(arr, cmp)
local is_missing = {}
local ok = true
for _, key in ipairs(arr) do
if cmp[key] == nil then
table.insert(is_missing, key)
ok = false
end
end
return ok, is_missing
end
return common

30
com/Access/_errors.lua Normal file
View File

@@ -0,0 +1,30 @@
-- File com/Access/_errors.lua
--
-- Created at 2025-21-10
-- Description:
--- Centralized error definitions for Access operations
--- to keep API responses consistent and clean.
local errors = {
-- Common validation
MISSING_PARAMS = { code = -32602, message = "Missing params" },
INVALID_FIELD_TYPE = { code = -32602, message = "'fields' must be a non-empty table" },
INVALID_BY_PARAM = { code = -32602, message = "Invalid 'by' param" },
NO_VALID_FIELDS = { code = -32604, message = "No valid fields to update" },
-- Existence / duplication
UNIT_NOT_FOUND = { code = -32102, message = "Unit is not exists" },
UNIT_EXISTS = { code = -32101, message = "Unit is already exists" },
-- Database & constraint
UNIQUE_CONSTRAINT = { code = -32602, message = "Unique constraint failed" },
DB_QUERY_FAILED = { code = -32001, message = "Database query failed" },
DB_EXEC_FAILED = { code = -32002, message = "Database execution failed" },
DB_INSERT_FAILED = { code = -32003, message = "Failed to create unit" },
DB_DELETE_FAILED = { code = -32004, message = "Failed to delete unit" },
-- Generic fallback
UNKNOWN = { code = -32099, message = "Unexpected internal error" },
}
return errors

View File

@@ -1,93 +0,0 @@
-- 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 params = session.request.params.get()
local token = session.request.headers.get("x-session-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 (token and token == require("_config").token()) then
return error_response("access denied")
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()

View File

@@ -1,74 +0,0 @@
-- 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 params = session.request.params.get()
local token = session.request.headers.get("x-session-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 (token and token == require("_config").token()) then
return error_response("access denied")
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
session.response.result = {
user = {
id = unit.id,
username = unit.username,
email = unit.email,
created_at = unit.created_at
}
}
close_db()

View File

@@ -1,84 +0,0 @@
-- 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 params = session.request.params.get()
local token = session.request.headers.get("x-session-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 (token and token == require("_config").token()) then
return error_response("access denied")
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 = ? OR phone_number = ?) LIMIT 1", {
params.email,
params.username,
params.phone_number
})
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()

11
com/Echo.lua Normal file
View 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)

View File

@@ -52,6 +52,6 @@ end
local basePath = "com"
local layer = params.layer and params.layer:gsub(">", "/") or nil
session.response.result = {
session.response.send({
answer = layer and scanDirectory(basePath, layer) or scanDirectory(basePath, "")
}
})

68
com/Unit/Create.lua Normal file
View File

@@ -0,0 +1,68 @@
-- File com/Unit/Create.lua
--
-- Created at 2025-05-10 18:23
--
-- Updated at -
-- Description:
--- Creates a record in the unit.db database without
--- requiring additional permissions. Requires username,
--- password (hashing occurs at the server level), and email fields.
local log = require("internal.log")
local db = require("internal.database.sqlite").connect("db/unit.db", {log = true})
local session = require("internal.session")
local crypt = require("internal.crypt.bcrypt")
local sha256 = require("internal.crypt.sha256")
local common = require("com/Unit/_common")
local errors = require("com/Unit/_errors")
-- Preparing for first db query
local function close_db()
if db then
log.debug("Closing DB connection")
db:close()
db = nil
end
end
local params = session.request.params.get()
local ok, mp = common.CheckMissingElement({"username", "password", "email"}, params)
if not ok then
close_db()
session.response.send_error(errors.MISSING_PARAMS.code, errors.MISSING_PARAMS.message, mp)
end
local hashPass = crypt.generate(params.password, crypt.DefaultCost)
local unitID = string.sub(sha256.hash(session.__seed), 1, 16)
local ctx, err = db:exec(
"INSERT INTO units (user_id, username, email, password) VALUES (?, ?, ?, ?)",
{
unitID,
params.username,
params.email,
hashPass,
}
)
if err ~= nil then
log.error("Insert failed: "..tostring(err))
close_db()
session.response.send_error(errors.DB_INSERT_FAILED.code, errors.DB_INSERT_FAILED.message)
end
local _, err = ctx:wait()
if err ~= nil then
close_db()
if tostring(err):match("UNIQUE constraint failed") then
session.response.send_error(errors.UNIT_EXISTS.code, errors.UNIT_EXISTS.message)
else
log.error("Insert confirmation failed: "..tostring(err))
session.response.send_error()
end
end
close_db()
session.response.send({unit_id = unitID})

77
com/Unit/Delete.lua Normal file
View File

@@ -0,0 +1,77 @@
-- File com/Unit/Delete.lua
--
-- Created at 2025-05-10 19:18
--
-- Updated at -
local log = require("internal.log")
local db = require("internal.database.sqlite").connect("db/unit.db", {log = true})
local session = require("internal.session")
local common = require("com/Unit/_common")
local errors = require("com/Unit/_errors")
-- Preparing for first db query
local function close_db()
if db then
log.debug("Closing DB connection")
db:close()
db = nil
end
end
local params = session.request.params.get()
local ok, mp = common.CheckMissingElement({"user_id"}, params)
if not ok then
close_db()
session.response.send_error(errors.MISSING_PARAMS.code, errors.MISSING_PARAMS.message, mp)
end
local existing, err = db:query([[
SELECT 1
FROM units
WHERE user_id = ?
AND entry_status != 'deleted'
AND deleted_at IS NULL
LIMIT 1
]], {
params.user_id
})
if err ~= nil then
log.error("Email check failed: "..tostring(err))
close_db()
session.response.send_error()
end
if existing and #existing == 0 then
close_db()
session.response.send_error(errors.UNIT_NOT_FOUND.code, errors.UNIT_NOT_FOUND.message)
end
local ctx, err = db:exec(
[[
UPDATE units
SET entry_status = 'deleted',
deleted_at = CURRENT_TIMESTAMP
WHERE user_id = ? AND deleted_at is NULL
]],
{ params.user_id }
)
if err ~= nil then
log.error("Soft delete failed: " .. tostring(err))
close_db()
session.response.send_error(errors.DB_DELETE_FAILED.code, errors.DB_DELETE_FAILED.message)
end
local res, err = ctx:wait()
if err ~= nil then
log.error("Soft delete confirmation failed: " .. tostring(err))
close_db()
session.response.send_error(errors.DB_DELETE_FAILED.code, errors.DB_DELETE_FAILED.message)
end
close_db()
session.response.send()

55
com/Unit/Get.lua Normal file
View File

@@ -0,0 +1,55 @@
-- File com/Unit/Get.lua
--
-- Created at 2025-09-25 20:04
--
-- Updated at -
local log = require("internal.log")
local db = require("internal.database.sqlite").connect("db/unit.db", {log = true})
local session = require("internal.session")
local common = require("com/Unit/_common")
local errors = require("com/Unit/_errors")
-- Preparing for first db query
local function close_db()
if db then
log.debug("Closing DB connection")
db:close()
db = nil
end
end
local params = session.request.params.get()
local ok, mp = common.CheckMissingElement({"by", "value"}, params)
if not ok then
close_db()
session.response.send_error(errors.MISSING_PARAMS.code, errors.MISSING_PARAMS.message, mp)
end
if not (params.by == "email" or params.by == "username" or params.by == "user_id") then
close_db()
session.response.send_error(errors.INVALID_BY_PARAM.code, errors.INVALID_BY_PARAM.message)
end
local unit, err = db:query_row(
"SELECT user_id, username, email, created_at, updated_at, deleted_at, entry_status FROM units WHERE "..params.by.." = ? AND deleted_at IS NULL LIMIT 1",
{
params.value
}
)
if err then
close_db()
log.error("DB query error: " .. tostring(err))
session.response.send_error()
end
if not unit then
close_db()
session.response.send_error(errors.UNIT_NOT_FOUND.code, errors.UNIT_NOT_FOUND.message)
end
close_db()
session.response.send(unit)

102
com/Unit/Update.lua Normal file
View File

@@ -0,0 +1,102 @@
-- File com/Unit/Update.lua
--
-- Created at 2025-10-10
--
local log = require("internal.log")
local db = require("internal.database.sqlite").connect("db/unit.db", { log = true })
local session = require("internal.session")
local common = require("com/Unit/_common")
local errors = require("com/Unit/_errors")
local function close_db()
if db then
log.debug("Closing DB connection")
db:close()
db = nil
end
end
local params = session.request.params.get()
local ok, mp = common.CheckMissingElement({"user_id", "fields"}, params)
if not ok then
close_db()
session.response.send_error(errors.MISSING_PARAMS.code, errors.MISSING_PARAMS.message, mp)
end
if type(params.fields) ~= "table" or next(params.fields) == nil then
close_db()
session.response.send_error(errors.INVALID_FIELD_TYPE.code, errors.INVALID_FIELD_TYPE.message)
end
local allowed = {
username = true,
email = true,
password = true,
entry_status = true
}
local exists = db:query_row(
"SELECT 1 FROM units WHERE user_id = ? AND deleted_at IS NULL LIMIT 1",
{ params.user_id }
)
if not exists then
close_db()
session.response.send_error(errors.UNIT_NOT_FOUND.code, errors.UNIT_NOT_FOUND.message)
end
local set_clauses = {}
local values = {}
for k, v in pairs(params.fields) do
if allowed[k] then
if k == "password" then
local crypt = require("internal.crypt.bcrypt")
v = crypt.generate(v, crypt.DefaultCost)
end
table.insert(set_clauses, k .. " = ?")
table.insert(values, v)
else
log.warn("Ignoring unsupported field: " .. k)
end
end
if #set_clauses == 0 then
close_db()
session.response.send_error(errors.NO_VALID_FIELDS.code, errors.NO_VALID_FIELDS.message)
end
table.insert(set_clauses, "updated_at = CURRENT_TIMESTAMP")
local query = "UPDATE units SET " .. table.concat(set_clauses, ", ")
.. " WHERE user_id = ? AND deleted_at IS NULL"
table.insert(values, params.user_id)
local ctx, err = db:exec(query, values)
if not ctx then
close_db()
if tostring(err):match("UNIQUE constraint failed") then
session.response.send_error(errors.UNIQUE_CONSTRAINT.code, errors.UNIQUE_CONSTRAINT.message)
else
session.response.send_error()
end
end
local _, err = ctx:wait()
if err ~= nil then
close_db()
if tostring(err):match("UNIQUE constraint failed") then
session.response.send_error(errors.UNIQUE_CONSTRAINT.code, errors.UNIQUE_CONSTRAINT.message)
else
log.error("Insert confirmation failed: "..tostring(err))
session.response.send_error()
end
end
close_db()
session.response.send()

23
com/Unit/_common.lua Normal file
View File

@@ -0,0 +1,23 @@
-- File com/Unit/_common.lua
--
-- Created at 2025-05-10 18:23
--
-- Updated at -
-- Description:
--- Common functions for Unit module
local common = {}
function common.CheckMissingElement(arr, cmp)
local is_missing = {}
local ok = true
for _, key in ipairs(arr) do
if cmp[key] == nil then
table.insert(is_missing, key)
ok = false
end
end
return ok, is_missing
end
return common

30
com/Unit/_errors.lua Normal file
View File

@@ -0,0 +1,30 @@
-- File com/Unit/_errors.lua
--
-- Created at 2025-10-10
-- Description:
--- Centralized error definitions for Unit operations
--- to keep API responses consistent and clean.
local errors = {
-- Common validation
MISSING_PARAMS = { code = -32602, message = "Missing params" },
INVALID_FIELD_TYPE = { code = -32602, message = "'fields' must be a non-empty table" },
INVALID_BY_PARAM = { code = -32602, message = "Invalid 'by' param" },
NO_VALID_FIELDS = { code = -32604, message = "No valid fields to update" },
-- Existence / duplication
UNIT_NOT_FOUND = { code = -32102, message = "Unit is not exists" },
UNIT_EXISTS = { code = -32101, message = "Unit is already exists" },
-- Database & constraint
UNIQUE_CONSTRAINT = { code = -32602, message = "Unique constraint failed" },
DB_QUERY_FAILED = { code = -32001, message = "Database query failed" },
DB_EXEC_FAILED = { code = -32002, message = "Database execution failed" },
DB_INSERT_FAILED = { code = -32003, message = "Failed to create unit" },
DB_DELETE_FAILED = { code = -32004, message = "Failed to delete unit" },
-- Generic fallback
UNKNOWN = { code = -32099, message = "Unexpected internal error" },
}
return errors

View File

@@ -1,66 +0,0 @@
---@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()

View File

@@ -1,29 +0,0 @@
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"
}

View File

@@ -1,35 +0,0 @@
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"
}

View File

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

View File

@@ -1,30 +0,0 @@
node:
mode: dev
name: "My gosally node"
show_config: true
com_dir: "%path%/com"
http_server:
address: "0.0.0.0"
port: "8080"
session_ttl: 5s
timeout: 3s
idle_timeout: 30s
tls:
enabled: true
cert_file: "%path%/cert/fullchain.pem"
key_file: "%path%/cert/privkey.pem"
updates:
enabled: false
check-interval: 1h
repository_url: "https://repo.serve.lv/raw/go-sally"
log:
json_format: false
level: "debug"
disable_warnings:
- --WNonStdTmpDir
- --WUndefLogLevel

View File

@@ -1,33 +0,0 @@
package rpc
import "encoding/json"
func NewError(code int, message string, data any, id *json.RawMessage) *RPCResponse {
if data != nil {
return &RPCResponse{
JSONRPC: JSONRPCVersion,
ID: id,
Error: map[string]any{
"code": code,
"message": message,
"data": data,
},
}
}
return &RPCResponse{
JSONRPC: JSONRPCVersion,
ID: id,
Error: map[string]any{
"code": code,
"message": message,
},
}
}
func NewResponse(result any, id *json.RawMessage) *RPCResponse {
return &RPCResponse{
JSONRPC: JSONRPCVersion,
ID: id,
Result: result,
}
}

View File

@@ -1,128 +0,0 @@
package sv1
import (
"fmt"
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)
var arr []any
isArray := true
tbl.ForEach(func(key, val lua.LValue) {
if key.Type() != lua.LTNumber {
isArray = false
}
arr = append(arr, ConvertLuaTypesToGolang(val))
})
if isArray {
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 {
switch v := val.(type) {
case nil:
return lua.LNil
case string:
return lua.LString(v)
case bool:
return lua.LBool(v)
case int:
return lua.LNumber(v)
case int8:
return lua.LNumber(v)
case int16:
return lua.LNumber(v)
case int32:
return lua.LNumber(v)
case int64:
return lua.LNumber(v)
case uint:
return lua.LNumber(v)
case uint8:
return lua.LNumber(v)
case uint16:
return lua.LNumber(v)
case uint32:
return lua.LNumber(v)
case uint64:
return lua.LNumber(v)
case float32:
return lua.LNumber(v)
case float64:
return lua.LNumber(v)
case []string:
tbl := L.NewTable()
for i, s := range v {
tbl.RawSetInt(i+1, lua.LString(s))
}
return tbl
case []int:
tbl := L.NewTable()
for i, n := range v {
tbl.RawSetInt(i+1, lua.LNumber(n))
}
return tbl
case []float64:
tbl := L.NewTable()
for i, f := range v {
tbl.RawSetInt(i+1, lua.LNumber(f))
}
return tbl
case []any:
tbl := L.NewTable()
for i, item := range v {
tbl.RawSetInt(i+1, ConvertGolangTypesToLua(L, item))
}
return tbl
case map[string]string:
tbl := L.NewTable()
for k, s := range v {
tbl.RawSetString(k, lua.LString(s))
}
return tbl
case map[string]int:
tbl := L.NewTable()
for k, n := range v {
tbl.RawSetString(k, lua.LNumber(n))
}
return tbl
case map[string]any:
tbl := L.NewTable()
for k, val := range v {
tbl.RawSetString(k, ConvertGolangTypesToLua(L, val))
}
return tbl
default:
return lua.LString(fmt.Sprintf("%v", v))
}
}

10
main.go
View File

@@ -1,10 +0,0 @@
package main
import (
"github.com/akyaiy/GoSally-mvp/cmd"
_ "modernc.org/sqlite"
)
func main() {
cmd.Execute()
}

View File

@@ -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,9 +7,9 @@ import (
"log"
"os"
"github.com/akyaiy/GoSally-mvp/hooks"
"github.com/akyaiy/GoSally-mvp/internal/colors"
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/src/hooks"
"github.com/akyaiy/GoSally-mvp/src/internal/colors"
"github.com/akyaiy/GoSally-mvp/src/internal/core/corestate"
"github.com/spf13/cobra"
)
@@ -22,6 +24,8 @@ scripts in a given directory. For more information, visit: https://gosally.oblat
},
}
// Execute prepares global log, loads cmdline args
// and executes rootCmd.Execute()
func Execute() {
log.SetOutput(os.Stdout)
log.SetPrefix(colors.SetBrightBlack(fmt.Sprintf("(%s) ", corestate.StageNotReady)))

View File

@@ -1,7 +1,7 @@
package cmd
import (
"github.com/akyaiy/GoSally-mvp/hooks"
"github.com/akyaiy/GoSally-mvp/src/hooks"
"github.com/spf13/cobra"
)
@@ -11,6 +11,7 @@ var runCmd = &cobra.Command{
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,
}

View File

@@ -4,7 +4,8 @@ import (
"fmt"
"runtime"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/src/internal/server/sv1"
"github.com/spf13/cobra"
)
@@ -13,7 +14,8 @@ var verCmd = &cobra.Command{
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 Sally version: %s\n", config.NodeVersion)
fmt.Printf("sv1 version: %s\n", sv1.SV1Version)
fmt.Printf("Go version: %s\n", runtime.Version())
fmt.Printf("Go OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
},

View File

@@ -1,4 +1,4 @@
module github.com/akyaiy/GoSally-mvp
module github.com/akyaiy/GoSally-mvp/src
go 1.24.4
@@ -19,6 +19,7 @@ require (
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/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect

View File

@@ -13,6 +13,8 @@ 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.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
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=

View File

@@ -18,16 +18,17 @@ import (
"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"
"github.com/akyaiy/GoSally-mvp/src/internal/colors"
"github.com/akyaiy/GoSally-mvp/src/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/src/internal/core/run_manager"
"github.com/akyaiy/GoSally-mvp/src/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/app"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/src/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) {
@@ -65,7 +66,8 @@ func InitConfigLoadHook(_ context.Context, cs *corestate.CoreState, x *app.AppX)
}
}
func InitUUUDHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
// 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 {
@@ -80,8 +82,11 @@ func InitUUUDHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
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
@@ -133,6 +138,8 @@ func InitRuntimeHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
}
// 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...")
@@ -185,6 +192,8 @@ func InitRunlockHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
}
}
// 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")
@@ -209,6 +218,8 @@ func InitConfigReplHook(_ context.Context, cs *corestate.CoreState, x *app.AppX)
}
}
// 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)
@@ -223,7 +234,11 @@ func InitConfigPrintHook(ctx context.Context, cs *corestate.CoreState, x *app.Ap
}
}
x.Log.Printf("Starting \"%s\" node", *x.Config.Conf.Node.Name)
if *x.Config.Conf.Node.Name == "noname" {
x.Log.Printf("Starting node")
} else {
x.Log.Printf("Starting \"%s\" node", *x.Config.Conf.Node.Name)
}
}
func InitSLogHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
@@ -239,6 +254,8 @@ func InitSLogHook(_ context.Context, cs *corestate.CoreState, x *app.AppX) {
*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 {

View File

@@ -11,17 +11,18 @@ import (
"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/akyaiy/GoSally-mvp/src/internal/colors"
"github.com/akyaiy/GoSally-mvp/src/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/src/internal/core/run_manager"
"github.com/akyaiy/GoSally-mvp/src/internal/core/update"
"github.com/akyaiy/GoSally-mvp/src/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/app"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/logs"
"github.com/akyaiy/GoSally-mvp/src/internal/server/gateway"
"github.com/akyaiy/GoSally-mvp/src/internal/server/session"
"github.com/akyaiy/GoSally-mvp/src/internal/server/sv1"
"github.com/akyaiy/GoSally-mvp/src/internal/server/sv2"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/spf13/cobra"
@@ -29,11 +30,12 @@ import (
)
var NodeApp = app.New()
var AllowedCmdPattern = `^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*$`
func Run(cmd *cobra.Command, args []string) {
NodeApp.InitialHooks(
InitGlobalLoggerHook, InitCorestateHook, InitConfigLoadHook,
InitUUUDHook, InitRuntimeHook, InitRunlockHook,
InitUUIDHook, InitRuntimeHook, InitRunlockHook,
InitConfigReplHook, InitConfigPrintHook, InitSLogHook,
)
@@ -60,17 +62,24 @@ func RunHook(ctx context.Context, cs *corestate.CoreState, x *app.AppX) error {
serverv1 := sv1.InitV1Server(&sv1.HandlerV1InitStruct{
X: x,
CS: cs,
AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+(>[a-zA-Z0-9]+)*$`),
AllowedCmd: regexp.MustCompile(AllowedCmdPattern),
Ver: "v1",
})
sv2 := sv2.InitServer(&sv2.HandlerInitStruct{
X: x,
CS: cs,
AllowedCmd: regexp.MustCompile(AllowedCmdPattern),
Ver: "v2",
})
session_manager := session.New(*x.Config.Conf.HTTPServer.SessionTTL)
s := gateway.InitGateway(&gateway.GatewayServerInit{
SM: session_manager,
CS: cs,
X: x,
}, serverv1)
}, serverv1, sv2)
r := chi.NewRouter()
r.Use(cors.Handler(cors.Options{

View File

@@ -1,5 +1,7 @@
package corestate
var NODE_UUID string
type Stage string
const (

View File

@@ -7,8 +7,8 @@ import (
"path/filepath"
"strings"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/src/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/config"
)
// GetNodeUUID outputs the correct uuid from the file at the path specified in the arguments.

View File

@@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/src/internal/core/utils"
)
type RunManagerContract interface {

View File

@@ -13,10 +13,10 @@ import (
"strings"
"syscall"
"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/src/internal/core/run_manager"
"github.com/akyaiy/GoSally-mvp/src/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/app"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/config"
"golang.org/x/net/context"
)

View File

@@ -5,7 +5,7 @@ import (
"encoding/hex"
"errors"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/config"
)
func NewUUIDRaw(length int) ([]byte, error) {

View File

@@ -9,8 +9,8 @@ import (
"sync"
"syscall"
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/src/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/config"
)
type AppContract interface {

View File

@@ -61,6 +61,7 @@ func (c *Compositor) LoadConf(path string) error {
v.SetDefault("log.json_format", "false")
v.SetDefault("log.level", "info")
v.SetDefault("log.output", "%2%")
v.SetDefault("disable_warnings", []string{})
if err := v.ReadInConfig(); err != nil {
return fmt.Errorf("error reading config: %w", err)

View File

@@ -5,7 +5,7 @@ import (
"reflect"
"time"
"github.com/akyaiy/GoSally-mvp/internal/colors"
"github.com/akyaiy/GoSally-mvp/src/internal/colors"
)
func (c *Compositor) Print(v any) {

View File

@@ -11,7 +11,7 @@ import (
"os"
"path/filepath"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/config"
"gopkg.in/natefinch/lumberjack.v2"
)

View File

@@ -0,0 +1 @@
package lua

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

View File

@@ -0,0 +1,25 @@
package lua
import (
"net/http"
"github.com/akyaiy/GoSally-mvp/src/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/app"
"github.com/akyaiy/GoSally-mvp/src/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
}

View File

@@ -4,10 +4,10 @@ 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"
"github.com/akyaiy/GoSally-mvp/src/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/app"
"github.com/akyaiy/GoSally-mvp/src/internal/server/rpc"
"github.com/akyaiy/GoSally-mvp/src/internal/server/session"
)
// serversApiVer is a type alias for string, used to represent API version strings in the GeneralServer.

View File

@@ -3,9 +3,9 @@ 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"
"github.com/akyaiy/GoSally-mvp/src/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/app"
"github.com/akyaiy/GoSally-mvp/src/internal/server/session"
)
// GeneralServerInit structure only for initialization general server.

View File

@@ -8,8 +8,8 @@ import (
"net/http"
"sync"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
"github.com/akyaiy/GoSally-mvp/src/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/src/internal/server/rpc"
"github.com/google/uuid"
)
@@ -20,18 +20,14 @@ func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
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.RPCResponse{
Error: map[string]any{
"code": rpc.ErrSessionIsBusy,
"message": rpc.ErrSessionIsBusyS,
},
})
rpc.WriteError(w, rpc.NewError(rpc.ErrSessionIsBusy, rpc.ErrSessionIsBusyS, nil, nil))
return
}
defer gs.sm.Delete(sessionUUID)
@@ -40,14 +36,7 @@ func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
if err != nil {
gs.x.SLog.Debug("failed to read body", slog.String("err", err.Error()))
w.WriteHeader(http.StatusBadRequest)
rpc.WriteError(w, &rpc.RPCResponse{
JSONRPC: rpc.JSONRPCVersion,
ID: nil,
Error: map[string]any{
"code": rpc.ErrInternalError,
"message": rpc.ErrInternalErrorS,
},
})
rpc.WriteError(w, rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, nil))
gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrInternalErrorS))
return
}
@@ -60,14 +49,7 @@ func (gs *GatewayServer) Handle(w http.ResponseWriter, r *http.Request) {
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.RPCResponse{
JSONRPC: rpc.JSONRPCVersion,
ID: nil,
Error: map[string]any{
"code": rpc.ErrParseError,
"message": rpc.ErrParseErrorS,
},
})
rpc.WriteError(w, rpc.NewError(rpc.ErrParseError, rpc.ErrParseErrorS, nil, nil))
gs.x.SLog.Info("invalid request received", slog.String("issue", rpc.ErrParseErrorS))
return
}

View File

@@ -13,8 +13,16 @@ type RPCRequest struct {
type RPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID *json.RawMessage `json:"id"`
Result any `json:"result,omitempty"`
Error any `json:"error,omitempty"`
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 (

View File

@@ -0,0 +1,60 @@
package rpc
import (
"crypto/md5"
"encoding/json"
"fmt"
"github.com/akyaiy/GoSally-mvp/src/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,
}
if data != nil {
Error["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),
}
}

View File

@@ -44,7 +44,7 @@ func getDBMutex(dbPath string) *sync.RWMutex {
return mtx
}
func loadDBMod(llog *slog.Logger) func(*lua.LState) int {
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()
@@ -80,12 +80,13 @@ func loadDBMod(llog *slog.Logger) func(*lua.LState) int {
mt := L.NewTypeMetatable("gosally_db")
L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{
"exec": dbExec,
"query": dbQuery,
"close": dbClose,
"exec": dbExec,
"query": dbQuery,
"query_row": dbQueryRow,
"close": dbClose,
}))
L.SetField(dbMod, "__gosally_internal", lua.LString("0"))
L.SetField(dbMod, "__seed", lua.LString(sid))
L.Push(dbMod)
return 1
}
@@ -213,6 +214,102 @@ func dbExec(L *lua.LState) int {
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)

View File

@@ -5,7 +5,7 @@ import (
"log/slog"
"net/http"
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
"github.com/akyaiy/GoSally-mvp/src/internal/server/rpc"
)
func (h *HandlerV1) Handle(_ context.Context, sid string, r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse {
@@ -24,6 +24,16 @@ func (h *HandlerV1) Handle(_ context.Context, sid string, r *http.Request, req *
return rpc.NewError(rpc.ErrMethodNotFound, rpc.ErrMethodNotFoundS, nil, req.ID)
}
}
return h.handleLUA(sid, r, req, method)
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)
}
}

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

View File

@@ -3,6 +3,8 @@ package sv1
// TODO: make a lua state pool using sync.Pool
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log/slog"
@@ -15,8 +17,8 @@ import (
"golang.org/x/crypto/bcrypt"
"github.com/akyaiy/GoSally-mvp/internal/colors"
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
"github.com/akyaiy/GoSally-mvp/src/internal/colors"
"github.com/akyaiy/GoSally-mvp/src/internal/server/rpc"
lua "github.com/yuin/gopher-lua"
_ "modernc.org/sqlite"
)
@@ -41,11 +43,29 @@ func addInitiatorHeaders(sid string, req *http.Request, headers http.Header) {
// 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 {
@@ -59,7 +79,7 @@ func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest,
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)
@@ -82,14 +102,24 @@ func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest,
}
return 1
})
L.SetField(headersTable, "__fetched", fetchedHeadersTable)
L.SetField(headersTable, "get", headersGetter)
L.SetField(inTable, "headers", headersTable)
fetchedParamsTable := L.NewTable()
if fetchedParams, ok := req.Params.(map[string]any); ok {
for k, v := range fetchedParams {
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)
@@ -106,16 +136,17 @@ func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest,
if tblVal, ok := val.(*lua.LTable); ok {
current = tblVal
} else {
if index == size - 1 {
if index == size-1 {
return val
}
return lua.LNil
}
}
return lua.LNil
return current
}
val := get(fetchedParamsTable, path)
paramsTbl := L.GetField(paramsTable, "__fetched") //
val := get(paramsTbl.(*lua.LTable), path) //
if val == lua.LNil && def != lua.LNil {
L.Push(def)
} else {
@@ -123,27 +154,133 @@ func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest,
}
return 1
})
L.SetField(headersTable, "__fetched", fetchedHeadersTable)
L.SetField(headersTable, "get", headersGetter)
L.SetField(inTable, "headers", headersTable)
L.SetField(paramsTable, "__fetched", fetchedParamsTable)
L.SetField(paramsTable, "get", paramsGetter)
L.SetField(inTable, "params", paramsTable)
outTable := L.NewTable()
resultTable := L.NewTable()
L.SetField(outTable, "result", resultTable)
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)
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, "__gosally_internal", lua.LString(fmt.Sprint(seed)))
L.SetField(sessionMod, "__seed", lua.LString(fmt.Sprint(seed)))
L.Push(sessionMod)
return 1
}
@@ -190,7 +327,7 @@ func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest,
}))
}
L.SetField(logMod, "__gosally_internal", lua.LString(fmt.Sprint(seed)))
L.SetField(logMod, "__seed", lua.LString(fmt.Sprint(seed)))
L.Push(logMod)
return 1
}
@@ -319,7 +456,7 @@ func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest,
L.SetField(netMod, "http", netModhttp)
L.SetField(netMod, "__gosally_internal", lua.LString(fmt.Sprint(seed)))
L.SetField(netMod, "__seed", lua.LString(fmt.Sprint(seed)))
L.Push(netMod)
return 1
}
@@ -391,16 +528,38 @@ func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest,
return 1
}))
L.SetField(bcryptMod, "__gosally_internal", lua.LString(fmt.Sprint(seed)))
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, "hash", 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))
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")
@@ -411,7 +570,8 @@ func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest,
}
}
llog.Debug("executing script", slog.String("script", path))
if err := L.DoFile(path); err != nil {
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)
}
@@ -433,17 +593,13 @@ func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest,
sessionVal := loadedTbl.RawGetString("internal.session")
sessionTbl, ok := sessionVal.(*lua.LTable)
if !ok {
return rpc.NewResponse(map[string]any{
"responsible-node": h.cs.UUID32,
}, req.ID)
return rpc.NewResponse(nil, req.ID)
}
tag := sessionTbl.RawGetString("__gosally_internal")
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(map[string]any{
"responsible-node": h.cs.UUID32,
}, req.ID)
return rpc.NewResponse(nil, req.ID)
}
outVal := sessionTbl.RawGetString("response")
@@ -453,39 +609,28 @@ func (h *HandlerV1) handleLUA(sid string, r *http.Request, req *rpc.RPCRequest,
return rpc.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
}
if errVal := outTbl.RawGetString("error"); errVal != lua.LNil {
llog.Debug("catch error table", slog.String("script", path))
if errTbl, ok := errVal.(*lua.LTable); ok {
code := rpc.ErrInternalError
message := rpc.ErrInternalErrorS
data := make(map[string]any)
if c := errTbl.RawGetString("code"); c.Type() == lua.LTNumber {
code = int(c.(lua.LNumber))
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)
}
if msg := errTbl.RawGetString("message"); msg.Type() == lua.LTString {
message = msg.String()
}
rawData := errTbl.RawGetString("data")
if tbl, ok := rawData.(*lua.LTable); ok {
tbl.ForEach(func(k, v lua.LValue) { data[k.String()] = ConvertLuaTypesToGolang(v) })
} else {
llog.Error("the script terminated with an error", slog.String("code", strconv.Itoa(code)), slog.String("message", message))
return rpc.NewError(code, message, ConvertLuaTypesToGolang(rawData), req.ID)
}
llog.Error("the script terminated with an error", slog.String("code", strconv.Itoa(code)), slog.String("message", message))
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.NewError(rpc.ErrInternalError, rpc.ErrInternalErrorS, nil, req.ID)
}
resultVal := outTbl.RawGetString("result")
payload := make(map[string]any)
if tbl, ok := resultVal.(*lua.LTable); ok {
tbl.ForEach(func(k, v lua.LValue) { payload[k.String()] = ConvertLuaTypesToGolang(v) })
} else {
payload["message"] = ConvertLuaTypesToGolang(resultVal)
}
payload["responsible-node"] = h.cs.UUID32
return rpc.NewResponse(payload, req.ID)
return rpc.NewResponse(nil, req.ID)
}

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

View File

@@ -6,15 +6,17 @@ import (
"path/filepath"
"strings"
"github.com/akyaiy/GoSally-mvp/internal/server/rpc"
"github.com/akyaiy/GoSally-mvp/src/internal/server/rpc"
)
var RPCMethodSeparator = "."
func (h *HandlerV1) resolveMethodPath(method string) (string, error) {
if !h.allowedCmd.MatchString(method) {
return "", errors.New(rpc.ErrInvalidMethodFormatS)
}
parts := strings.Split(method, ">")
parts := strings.Split(method, RPCMethodSeparator)
relPath := filepath.Join(parts...) + ".lua"
fullPath := filepath.Join(*h.x.Config.Conf.Node.ComDir, relPath)

View File

@@ -5,10 +5,12 @@ package sv1
import (
"regexp"
"github.com/akyaiy/GoSally-mvp/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/internal/engine/app"
"github.com/akyaiy/GoSally-mvp/src/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/app"
)
var SV1Version = "v1"
// HandlerV1InitStruct structure is only for initialization
type HandlerV1InitStruct struct {
Ver string

View File

@@ -0,0 +1,12 @@
package sv2
import (
"context"
"net/http"
"github.com/akyaiy/GoSally-mvp/src/internal/server/rpc"
)
func (h *Handler) Handle(_ context.Context, sid string, r *http.Request, req *rpc.RPCRequest) *rpc.RPCResponse {
return nil
}

View File

@@ -0,0 +1,43 @@
// SV2 works with binaries, scripts, and anything else that has access to stdin/stdout.
// Modules run in a separate process and communicate via I/O.
package sv2
import (
"regexp"
"github.com/akyaiy/GoSally-mvp/src/internal/core/corestate"
"github.com/akyaiy/GoSally-mvp/src/internal/engine/app"
)
// HandlerV2InitStruct structure is only for initialization
type HandlerInitStruct struct {
Ver string
CS *corestate.CoreState
X *app.AppX
AllowedCmd *regexp.Regexp
}
type Handler struct {
cs *corestate.CoreState
x *app.AppX
// allowedCmd and listAllowedCmd are regular expressions used to validate command names.
allowedCmd *regexp.Regexp
ver string
}
func InitServer(o *HandlerInitStruct) *Handler {
return &Handler{
cs: o.CS,
x: o.X,
allowedCmd: o.AllowedCmd,
ver: o.Ver,
}
}
// GetVersion returns the API version of the HandlerV1, which is set during initialization.
// This version is used to identify the API version in the request routing.
func (h *Handler) GetVersion() string {
return h.ver
}

11
src/main.go Normal file
View File

@@ -0,0 +1,11 @@
// Package main used only for calling cmd.Execute()
package main
import (
"github.com/akyaiy/GoSally-mvp/src/cmd"
_ "modernc.org/sqlite"
)
func main() {
cmd.Execute()
}