diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5587527 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +DISABLE_GLOOMI=false +GLOOMI_HOSTNAME=localhost diff --git a/.gitignore b/.gitignore index 7791b14..17521a8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -plugs/** \ No newline at end of file +plugs/** +gloom +**/*.so +.env +gloom.db \ No newline at end of file diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..67c9703 --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,56 @@ +# GLoom Documentation + +## Plugins + +Plugins are the core of GLoom, they are responsible for handling requests and providing routes. + +### Plugin Interface + +The `Plugin` interface is the main interface for plugins to implement. It has three methods: + +- `Init()` - This method is called when the plugin is loaded. It is the function that is initially called when the plugin is loaded. +- `Name()` - This method returns the name of the plugin. +- `RegisterRoutes(router fiber.Router)` - This method is called when the plugin is loaded and is responsible for registering routes to the router. + +An example plugin is provided in the `plugin` directory and can be built using the following command: + +```bash +zqdgr build +``` + +This will generate a `plugin.so` file in the `plugin` directory. + +## RPC + +GLoom exposes an RPC server on port 7143. This server is used for GLoom's plugin management system. Gloom currently provides two methods: + +- `ListPlugins(struct{}, reply *[]PluginData) error` - This method returns a list of all registered plugins and their domains. +- `UploadPlugin(PluginUpload, reply *string) error` - This method uploads a plugin to GLoom. + +PluginData is a struct that looks like this: + +```go +type PluginData struct { + Name string `json:"name"` + Domains []string `json:"domains"` +} +``` + +PluginUpload is a struct that looks like this: + +```go +type PluginUpload struct { + Name string `json:"name"` + Domains []string `json:"domains"` + Data []byte `json:"data"` +} +``` + +## GLoomI + +GLoomI is the included plugin management interface for GLoom, it utilizes the GLoom RPC, much like you would if you wanted to make your own management interface. By default, GLoomI is configured to use 127.0.0.1 as the hostname, but you con configure it to use a different hostname by setting the `GLOOMI_HOSTNAME` environment variable. The endpoints for GLoomI are as follows: + +- `GET /api/plugins` - This endpoint returns a list of all registered plugins and their domains. +- `POST /api/plugins` - This endpoint uploads a plugin to GLoom. it takes a multipart/form-data request with the following fields: + - `plugin` - The plugin file to upload. + - `domains` - A comma-separated list of domains to associate with the plugin. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f702aa4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 juls0730 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2df9d4a --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# GLoom + +GLoom is a plugin-based web server written in Go. GLoom's focus is to provide and simple and efficient way to host micro-web apps easily. + +## Features + +- Plugin-based architecture +- RPC-based communication between GLoom and plugins +- Built-in plugin management system +- Built-in plugin management UI + +## Getting Started + +### Prerequisites + +- Go 1.20 or higher +- [zqdgr](https://github.com/juls0730/zqdgr) + +### Installation + +1. Clone the repository: + +```bash +git clone https://github.com/juls0730/gloom.git +``` + +2. Run the project: + +```bash +zqdgr run +``` + +or if you want to build the project: + +```bash +zqdgr build +``` + +## Usage + +please read [DOCUMENTATION.md](DOCUMENTATION.md) + +## Contributing + +Contributions are welcome! + +## License + +GLoom is licensed under the MIT License. \ No newline at end of file diff --git a/gloomi/README.md b/gloomi/README.md new file mode 100644 index 0000000..0b9d633 --- /dev/null +++ b/gloomi/README.md @@ -0,0 +1,3 @@ +# GLoomI + +GLoomI is the management Interface for GLoom. This plugin is responsible for managing GLoom's plugins and domains, and utilizes gloom's built in plugin system. It uses RPC to communicate with GLoom. \ No newline at end of file diff --git a/gloomi/go.mod b/gloomi/go.mod new file mode 100644 index 0000000..486b3f4 --- /dev/null +++ b/gloomi/go.mod @@ -0,0 +1,26 @@ +module github.com/juls0730/gloomi + +go 1.23.4 + +require github.com/gofiber/fiber/v3 v3.0.0-beta.4 + +require ( + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gofiber/schema v1.2.0 // indirect + github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/tinylib/msgp v1.2.5 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.58.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect +) diff --git a/gloomi/go.sum b/gloomi/go.sum new file mode 100644 index 0000000..092a83e --- /dev/null +++ b/gloomi/go.sum @@ -0,0 +1,42 @@ +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0= +github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk= +github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg= +github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= +github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= +github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= +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/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= +github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE= +github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= diff --git a/gloomi/main.go b/gloomi/main.go new file mode 100644 index 0000000..2b60da1 --- /dev/null +++ b/gloomi/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + "io" + "mime/multipart" + "net/rpc" + "strings" + + "github.com/gofiber/fiber/v3" +) + +type GLoomI struct { + client *rpc.Client +} + +func (p *GLoomI) Init() error { + // Connect to the RPC server + client, err := rpc.Dial("tcp", "localhost:7143") + if err != nil { + return fmt.Errorf("failed to connect to Gloom RPC server: %w", err) + } + p.client = client + return nil +} + +func (p *GLoomI) Name() string { + return "GLoomI" +} + +type PluginData struct { + Name string `json:"name"` + Domains []string `json:"domains"` +} + +func GetPlugins(client *rpc.Client) ([]PluginData, error) { + var plugins []PluginData + err := client.Call("GloomRPC.ListPlugins", struct{}{}, &plugins) + + return plugins, err +} + +func (p *GLoomI) RegisterRoutes(router fiber.Router) { + apiRouter := router.Group("/api") + { + apiRouter.Get("/plugins", func(c fiber.Ctx) error { + plugins, err := GetPlugins(p.client) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to list plugins: " + err.Error()) + } + + return c.Status(fiber.StatusOK).JSON(plugins) + }) + + type UploadRequest struct { + Domains string `form:"domains"` + } + + apiRouter.Post("/plugins", func(c fiber.Ctx) error { + pluginUpload := new(UploadRequest) + if err := c.Bind().Form(pluginUpload); err != nil { + return c.Status(fiber.StatusBadRequest).SendString("Failed to bind form data: " + err.Error()) + } + + if pluginUpload.Domains == "" { + return c.Status(fiber.StatusBadRequest).SendString("No domains provided") + } + + domains := make([]string, 0) + for _, domain := range strings.Split(pluginUpload.Domains, ",") { + domains = append(domains, strings.TrimSpace(domain)) + } + + var pluginFile *multipart.FileHeader + pluginFile, err := c.FormFile("plugin") + if err != nil { + return c.Status(fiber.StatusBadRequest).SendString("Failed to get plugin file: " + err.Error()) + } + + pluginData, err := pluginFile.Open() + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to open plugin file: " + err.Error()) + } + + var pluginUploadStruct struct { + Domains []string `json:"domains"` + Name string `json:"name"` + Data []byte `json:"data"` + } + pluginUploadStruct.Name = pluginFile.Filename + pluginUploadStruct.Domains = domains + pluginUploadStruct.Data, err = io.ReadAll(pluginData) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to read plugin file: " + err.Error()) + } + + err = p.client.Call("GloomRPC.UploadPlugin", pluginUploadStruct, nil) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString("Failed to upload plugin: " + err.Error()) + } + + return c.Status(fiber.StatusOK).SendString("Plugin uploaded successfully") + }) + } +} + +// Exported symbol +var Plugin GLoomI diff --git a/gloomi/zqdgr.config.json b/gloomi/zqdgr.config.json new file mode 100644 index 0000000..01f5e47 --- /dev/null +++ b/gloomi/zqdgr.config.json @@ -0,0 +1,12 @@ +{ + "name": "Go Project", + "version": "0.0.1", + "description": "Example description", + "author": "you", + "license": "BSL-1.0", + "scripts": { + "build": "go build -buildmode=plugin -o ../plugs/gloomi.so main.go" + }, + "pattern": "**/*.go", + "excluded_dirs": [] +} \ No newline at end of file diff --git a/go.mod b/go.mod index ca9e3aa..c69c908 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/juls0730/gloom go 1.23.4 +require github.com/mattn/go-sqlite3 v1.14.24 + require ( github.com/andybalholm/brotli v1.1.1 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -9,6 +11,7 @@ require ( github.com/gofiber/schema v1.2.0 // indirect github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 092a83e..7d560a4 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/as github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= 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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -17,6 +19,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= diff --git a/libs/syncmap.go b/libs/syncmap.go new file mode 100644 index 0000000..e63f842 --- /dev/null +++ b/libs/syncmap.go @@ -0,0 +1,181 @@ +// https://gist.github.com/tarampampam/f96538257ff125ab71785710d48b3118 +package libs + +import "sync" + +// SyncMap is like a Go sync.Map but type-safe using generics. +// +// The zero SyncMap is empty and ready for use. A SyncMap must not be copied after first use. +type SyncMap[K comparable, V any] struct { + mu sync.Mutex + m map[K]V +} + +// Grow grows the map to the given size. It can be called before the first write operation used. +func (s *SyncMap[K, V]) Grow(size int) { + s.mu.Lock() + s.grow(size) + s.mu.Unlock() +} + +func (s *SyncMap[K, V]) grow(size ...int) { + if s.m == nil { + if len(size) == 0 { + s.m = make(map[K]V) // let runtime decide the needed map size + } else { + s.m = make(map[K]V, size[0]) + } + } +} + +// Clone returns a copy (clone) of current SyncMap. +func (s *SyncMap[K, V]) Clone() SyncMap[K, V] { + s.mu.Lock() + defer s.mu.Unlock() + + var clone = make(map[K]V, len(s.m)) + + for k, v := range s.m { + clone[k] = v + } + + return SyncMap[K, V]{m: clone} +} + +// Load returns the value stored in the map for a key, or nil if no value is present. +// The ok result indicates whether value was found in the map. +func (s *SyncMap[K, V]) Load(key K) (value V, loaded bool) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.m == nil { // fast operation terminator + return + } + + value, loaded = s.m[key] + + return +} + +// Store sets the value for a key. +func (s *SyncMap[K, V]) Store(key K, value V) { + s.mu.Lock() + defer s.mu.Unlock() + + s.grow() + + s.m[key] = value +} + +// LoadOrStore returns the existing value for the key if present. Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (s *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + s.mu.Lock() + defer s.mu.Unlock() + + if actual, loaded = s.m[key]; !loaded { + s.grow() + + s.m[key], actual = value, value + } + + return +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. The loaded result reports whether +// the key was present. +func (s *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.m == nil { // fast operation terminator + return + } + + s.grow() + + if value, loaded = s.m[key]; loaded { + delete(s.m, key) + } + + return +} + +// Delete deletes the value for a key. +func (s *SyncMap[K, V]) Delete(key K) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.m == nil { // fast operation terminator + return + } + + s.grow() + + delete(s.m, key) +} + +// Range calls f sequentially for each key and value present in the map. If f returns false, range stops the iteration. +// +// Range does not necessarily correspond to any consistent snapshot of the Map's contents: no key will be visited more +// than once. Range does not block other methods on the receiver; even f itself may call any method on m. +func (s *SyncMap[K, V]) Range(f func(key K, value V) (shouldContinue bool)) { + s.mu.Lock() + + if s.m == nil { // fast operation terminator + s.mu.Unlock() + + return + } + + s.grow() + + for k, v := range s.m { + s.mu.Unlock() + + if !f(k, v) { + return + } + + s.mu.Lock() + } + + s.mu.Unlock() +} + +// Len returns the count of values in the map. +func (s *SyncMap[K, V]) Len() (l int) { + s.mu.Lock() + l = len(s.m) + s.mu.Unlock() + + return +} + +// Keys return slice with all map keys. +func (s *SyncMap[K, V]) Keys() []K { + s.mu.Lock() + defer s.mu.Unlock() + + var keys, i = make([]K, len(s.m)), 0 + + for k := range s.m { + keys[i], i = k, i+1 + } + + return keys +} + +// Values return slice with all map values. +func (s *SyncMap[K, V]) Values() []V { + s.mu.Lock() + defer s.mu.Unlock() + + var values, i = make([]V, len(s.m)), 0 + + for _, v := range s.m { + values[i], i = v, i+1 + } + + return values +} diff --git a/main.go b/main.go index f6c34a9..274ab4e 100644 --- a/main.go +++ b/main.go @@ -1,90 +1,265 @@ package main import ( + "database/sql" + "embed" "fmt" + "log/slog" + "net" + "net/rpc" "os" "plugin" - "sync" + "strings" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/logger" + "github.com/joho/godotenv" + "github.com/juls0730/gloom/libs" + _ "github.com/mattn/go-sqlite3" ) +//go:embed schema.sql +var embeddedAssets embed.FS + type Plugin interface { - Name() string Init() error - Domains() []string + Name() string RegisterRoutes(app fiber.Router) } -var domainMap sync.Map // Maps domains to plugins - -func main() { - app := fiber.New() - - app.Use(logger.New()) - - plugins := loadPlugins() - - for _, p := range plugins { - for _, domain := range p.Domains() { - fmt.Printf("Registering domain: %s for plugin: %s\n", domain, p.Name()) - domainMap.Store(domain, p) - } - } - - app.Use(func(c fiber.Ctx) error { - host := c.Host() - if value, ok := domainMap.Load(host); ok { - plugin := value.(Plugin) - - pluginRouter := fiber.New() - plugin.RegisterRoutes(pluginRouter) - - pluginRouter.Handler()(c.RequestCtx()) - return nil - } - - return c.Status(404).SendString("Domain not found") - }) - - fmt.Println("Server running at http://localhost:3000") - app.Listen(":3000") +type PluginInstance struct { + Plugin Plugin + Name string + Router *fiber.App } -func loadPlugins() []Plugin { +type GLoom struct { + Plugins []PluginInstance + domainMap libs.SyncMap[string, *PluginInstance] + DB *sql.DB + fiber *fiber.App +} + +func NewGloom(app *fiber.App) (*GLoom, error) { if err := os.MkdirAll("plugs", 0755); err != nil { if os.IsNotExist(err) { panic(err) } } - pluginPaths := []string{"plugs/example.so"} - var plugins []Plugin - - for _, path := range pluginPaths { - p, err := plugin.Open(path) - if err != nil { - panic(err) - } - - symbol, err := p.Lookup("Plugin") - if err != nil { - panic(err) - } - - pluginInstance, ok := symbol.(Plugin) - if !ok { - panic("Invalid plugin type") - } - - err = pluginInstance.Init() - if err != nil { - panic(err) - } - - plugins = append(plugins, pluginInstance) + db, err := sql.Open("sqlite3", "gloom.db") + if err != nil { + return nil, err } - return plugins + schema, err := embeddedAssets.ReadFile("schema.sql") + if err != nil { + return nil, err + } + + _, err = db.Exec(string(schema)) + if err != nil { + return nil, err + } + + gloom := &GLoom{ + Plugins: []PluginInstance{}, + domainMap: libs.SyncMap[string, *PluginInstance]{}, + DB: db, + fiber: app, + } + + plugins, err := db.Query("SELECT path, domains FROM plugins") + if err != nil { + return nil, err + } + defer plugins.Close() + + for plugins.Next() { + var plugin struct { + Path string + Domain string + } + + if err := plugins.Scan(&plugin.Path, &plugin.Domain); err != nil { + return nil, err + } + + domains := strings.Split(plugin.Domain, ",") + + gloom.RegisterPlugin(plugin.Path, domains) + } + + return gloom, nil +} + +func (gloom *GLoom) RegisterPlugin(pluginPath string, domains []string) { + slog.Info("Registering plugin", "pluginPath", pluginPath, "domains", domains) + p, err := plugin.Open(pluginPath) + if err != nil { + panic(err) + } + + symbol, err := p.Lookup("Plugin") + if err != nil { + panic(err) + } + + pluginLib, ok := symbol.(Plugin) + if !ok { + panic("Plugin is not a Plugin") + } + + err = pluginLib.Init() + if err != nil { + panic(err) + } + + router := fiber.New() + pluginLib.RegisterRoutes(router) + + pluginInstance := PluginInstance{ + Plugin: pluginLib, + Name: pluginLib.Name(), + Router: router, + } + + gloom.Plugins = append(gloom.Plugins, pluginInstance) + pluginPtr := &gloom.Plugins[len(gloom.Plugins)-1] + for _, domain := range domains { + gloom.domainMap.Store(domain, pluginPtr) + } +} + +func (gloom *GLoom) StartRPCServer() error { + rpcServer := &GloomRPC{gloom: gloom} + err := rpc.Register(rpcServer) + if err != nil { + return err + } + + listener, err := net.Listen("tcp", ":7143") + if err != nil { + return err + } + + fmt.Printf("RPC server running on port 7143\n") + go func() { + for { + conn, err := listener.Accept() + if err != nil { + fmt.Println("RPC connection error:", err) + continue + } + go rpc.ServeConn(conn) + } + }() + + return nil +} + +type GloomRPC struct { + gloom *GLoom +} + +type PluginData struct { + Name string `json:"name"` + Domains []string `json:"domains"` +} + +// Example RPC method: List all registered plugins +func (rpc *GloomRPC) ListPlugins(_ struct{}, reply *[]PluginData) error { + var plugins []PluginData = make([]PluginData, 0) + var domains map[string][]string = make(map[string][]string) + + rpc.gloom.domainMap.Range(func(domain string, plugin *PluginInstance) bool { + domains[plugin.Name] = append(domains[plugin.Name], domain) + return true + }) + + for _, plugin := range rpc.gloom.Plugins { + var pluginDataStruct PluginData + pluginDataStruct.Name = plugin.Name + pluginDataStruct.Domains = domains[plugin.Name] + + plugins = append(plugins, pluginDataStruct) + } + + *reply = plugins + return nil +} + +type PluginUpload struct { + Name string `json:"name"` + Domains []string `json:"domains"` + Data []byte `json:"data"` +} + +func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error { + slog.Info("Uploading plugin", "plugin", plugin.Name, "domains", plugin.Domains) + if err := os.WriteFile(fmt.Sprintf("plugs/%s", plugin.Name), plugin.Data, 0644); err != nil { + return err + } + + fmt.Print("Plugin uploaded successfully") + + rpc.gloom.DB.Exec("INSERT INTO plugins (path, domains) VALUES (?, ?)", "plugs/"+plugin.Name, strings.Join(plugin.Domains, ",")) + rpc.gloom.RegisterPlugin("plugs/"+plugin.Name, plugin.Domains) + *reply = "Plugin uploaded successfully" + return nil +} + +func init() { + if err := godotenv.Load(); err != nil { + fmt.Println("No .env file found") + } +} + +func main() { + app := fiber.New(fiber.Config{ + BodyLimit: 1024 * 1024 * 1024 * 5, // 5GB + }) + + app.Use(logger.New(logger.Config{ + CustomTags: map[string]logger.LogFunc{ + "app": func(output logger.Buffer, c fiber.Ctx, data *logger.Data, extraParam string) (int, error) { + output.WriteString(c.Host()) + return len(output.Bytes()), nil + }, + }, + Format: " ${time} | ${status} | ${latency} | ${ip} | ${method} | ${app} | ${path}\n", + })) + + gloom, err := NewGloom(app) + if err != nil { + panic(err) + } + + app.Use(func(c fiber.Ctx) error { + host := c.Host() + if plugin, ok := gloom.domainMap.Load(host); ok { + plugin.Router.Handler()(c.RequestCtx()) + return nil + } + + return c.Status(404).SendString("Domain not found") + }) + + if err := gloom.StartRPCServer(); err != nil { + panic("Failed to start RPC server: " + err.Error()) + } + + if os.Getenv("DISABLE_GLOOMI") != "true" { + hostname := os.Getenv("GLOOMI_HOSTNAME") + if hostname == "" { + hostname = "127.0.0.1" + } + + gloom.RegisterPlugin("plugs/gloomi.so", []string{hostname}) + } + + fmt.Println("Server running at http://localhost:3000") + if err := app.Listen(":3000"); err != nil { + panic(err) + } } diff --git a/plugin/main.go b/plugin/main.go index 67fde4d..c7cf8e2 100644 --- a/plugin/main.go +++ b/plugin/main.go @@ -4,21 +4,17 @@ import "github.com/gofiber/fiber/v3" type MyPlugin struct{} -func (p MyPlugin) Name() string { - return "my plugin" -} - -func (p MyPlugin) Init() error { +func (p *MyPlugin) Init() error { return nil } -func (p MyPlugin) Domains() []string { - return []string{"myplugin.local"} +func (p *MyPlugin) Name() string { + return "MyPlugin" } -func (p MyPlugin) RegisterRoutes(router fiber.Router) { +func (p *MyPlugin) RegisterRoutes(router fiber.Router) { router.Get("/", func(c fiber.Ctx) error { - return c.SendString("Welcome to MyPlugin!") + return c.Status(fiber.StatusTeapot).SendString("Welcome to MyPlugin!") }) router.Get("/hello", func(c fiber.Ctx) error { diff --git a/plugin/zqdgr.config.json b/plugin/zqdgr.config.json index f91a927..668b58f 100644 --- a/plugin/zqdgr.config.json +++ b/plugin/zqdgr.config.json @@ -5,8 +5,7 @@ "author": "you", "license": "BSL-1.0", "scripts": { - "build": "go build -buildmode=plugin -o ../plugs/example.so main.go", - "dev": "go run main.go" + "build": "go build -buildmode=plugin -o plugin.so main.go" }, "pattern": "**/*.go", "excluded_dirs": [] diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..daca7c6 --- /dev/null +++ b/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS plugins ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + path TEXT NOT NULL, + domains TEXT NOT NULL +); diff --git a/zqdgr.config.json b/zqdgr.config.json index 9d8b103..c98af45 100644 --- a/zqdgr.config.json +++ b/zqdgr.config.json @@ -5,8 +5,8 @@ "author": "you", "license": "BSL-1.0", "scripts": { - "build": "go build", - "dev": "go run main.go" + "build": "sh -c \"cd gloomi; zqdgr build\" && go build", + "dev": "sh -c \"cd gloomi; zqdgr build\" && go run main.go" }, "pattern": "**/*.go", "excluded_dirs": []