Flesh out GLoom

I have documented GLoom's new RPC, how a plugin developer might create a plugin, and more. I have also created GLoomI, the included management interface for GLoom and added a LICENSE.
This commit is contained in:
Zoe
2025-01-07 19:33:08 +00:00
parent 3d6c4c72a7
commit 98c0f45ca8
18 changed files with 763 additions and 77 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
DISABLE_GLOOMI=false
GLOOMI_HOSTNAME=localhost

6
.gitignore vendored
View File

@@ -1 +1,5 @@
plugs/** plugs/**
gloom
**/*.so
.env
gloom.db

56
DOCUMENTATION.md Normal file
View File

@@ -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.

21
LICENSE Normal file
View File

@@ -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.

49
README.md Normal file
View File

@@ -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.

3
gloomi/README.md Normal file
View File

@@ -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.

26
gloomi/go.mod Normal file
View File

@@ -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
)

42
gloomi/go.sum Normal file
View File

@@ -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=

108
gloomi/main.go Normal file
View File

@@ -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

12
gloomi/zqdgr.config.json Normal file
View File

@@ -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": []
}

3
go.mod
View File

@@ -2,6 +2,8 @@ module github.com/juls0730/gloom
go 1.23.4 go 1.23.4
require github.com/mattn/go-sqlite3 v1.14.24
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // 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/schema v1.2.0 // indirect
github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect
github.com/google/uuid v1.6.0 // 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/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect

4
go.sum
View File

@@ -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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 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 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= 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 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=

181
libs/syncmap.go Normal file
View File

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

301
main.go
View File

@@ -1,90 +1,265 @@
package main package main
import ( import (
"database/sql"
"embed"
"fmt" "fmt"
"log/slog"
"net"
"net/rpc"
"os" "os"
"plugin" "plugin"
"sync" "strings"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/logger" "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 { type Plugin interface {
Name() string
Init() error Init() error
Domains() []string Name() string
RegisterRoutes(app fiber.Router) RegisterRoutes(app fiber.Router)
} }
var domainMap sync.Map // Maps domains to plugins type PluginInstance struct {
Plugin Plugin
func main() { Name string
app := fiber.New() Router *fiber.App
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")
} }
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 err := os.MkdirAll("plugs", 0755); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
panic(err) panic(err)
} }
} }
pluginPaths := []string{"plugs/example.so"} db, err := sql.Open("sqlite3", "gloom.db")
var plugins []Plugin if err != nil {
return nil, err
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)
} }
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)
}
} }

View File

@@ -4,21 +4,17 @@ import "github.com/gofiber/fiber/v3"
type MyPlugin struct{} type MyPlugin struct{}
func (p MyPlugin) Name() string { func (p *MyPlugin) Init() error {
return "my plugin"
}
func (p MyPlugin) Init() error {
return nil return nil
} }
func (p MyPlugin) Domains() []string { func (p *MyPlugin) Name() string {
return []string{"myplugin.local"} return "MyPlugin"
} }
func (p MyPlugin) RegisterRoutes(router fiber.Router) { func (p *MyPlugin) RegisterRoutes(router fiber.Router) {
router.Get("/", func(c fiber.Ctx) error { 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 { router.Get("/hello", func(c fiber.Ctx) error {

View File

@@ -5,8 +5,7 @@
"author": "you", "author": "you",
"license": "BSL-1.0", "license": "BSL-1.0",
"scripts": { "scripts": {
"build": "go build -buildmode=plugin -o ../plugs/example.so main.go", "build": "go build -buildmode=plugin -o plugin.so main.go"
"dev": "go run main.go"
}, },
"pattern": "**/*.go", "pattern": "**/*.go",
"excluded_dirs": [] "excluded_dirs": []

5
schema.sql Normal file
View File

@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS plugins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
domains TEXT NOT NULL
);

View File

@@ -5,8 +5,8 @@
"author": "you", "author": "you",
"license": "BSL-1.0", "license": "BSL-1.0",
"scripts": { "scripts": {
"build": "go build", "build": "sh -c \"cd gloomi; zqdgr build\" && go build",
"dev": "go run main.go" "dev": "sh -c \"cd gloomi; zqdgr build\" && go run main.go"
}, },
"pattern": "**/*.go", "pattern": "**/*.go",
"excluded_dirs": [] "excluded_dirs": []