add plugin hosts to seperate plugins from gloomi

This adds a plugin host that seperates plugins from the gloomi process,
allowing for plugins to be unloaded and loaded. This commit also has a
fair amount of other changes, nice to haves and bug fixes, some notable
changes are:
- Highly available reverse proxy from my Flux project
- Improved gloomi functionality
This commit is contained in:
Zoe
2025-05-14 19:31:58 -05:00
parent ad0e949070
commit b8f5bce66c
18 changed files with 852 additions and 239 deletions

View File

@@ -1,2 +1,3 @@
DISABLE_GLOOMI=false DISABLE_GLOOMI=false
GLOOMI_HOSTNAME=localhost GLOOMI_HOSTNAME=localhost
PLUGINS_DIR=plugs

3
.gitignore vendored
View File

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

View File

@@ -2,6 +2,9 @@
## Plugins ## Plugins
> [!IMPORTANT]
> Plugins __must__ be compiled with the same version of Go that GLoom was compiled with. This is a limitation of Golang's plugin system, which is what GLoom uses to load plugins.
Plugins are the core of GLoom, they are responsible for handling requests and providing routes. When building a plugin, it's expected that all the assets you need will be bundled with the plugin. However, you are allowed to create directories for assets like file uploads, but we urge you to create a specific directory for assets (ie a database or a public directory) to avoid cluttering the root directory of gloom. Plugins are the core of GLoom, they are responsible for handling requests and providing routes. When building a plugin, it's expected that all the assets you need will be bundled with the plugin. However, you are allowed to create directories for assets like file uploads, but we urge you to create a specific directory for assets (ie a database or a public directory) to avoid cluttering the root directory of gloom.
### Plugin Interface ### Plugin Interface
@@ -9,7 +12,6 @@ Plugins are the core of GLoom, they are responsible for handling requests and pr
The `Plugin` interface is the main interface for plugins to implement. It has three methods: The `Plugin` interface is the main interface for plugins to implement. It has three methods:
- `Init() (*fiber.Config, error)` - This method is called when the plugin is loaded. It is the function that is initially called when the plugin is loaded. - `Init() (*fiber.Config, error)` - This method is called when the plugin is loaded. It is the function that is initially called when the plugin is loaded.
- `Name() string` - 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. - `RegisterRoutes(router fiber.Router)` - This method is called when the plugin is loaded and is responsible for registering routes to the router.
Furthermore, your plugin should export a symbol named `Plugin` that implements the `Plugin` interface. The easiest way to do this in Go is simply Furthermore, your plugin should export a symbol named `Plugin` that implements the `Plugin` interface. The easiest way to do this in Go is simply

View File

@@ -1,18 +1,12 @@
# GLoom # GLoom
GLoom is a plugin-based web app manager written in Go. GLoom's focus is to provide and simple and efficient way to host micro-web apps easily. Currently, GLoom is a fun little proof of concept, but it suffers from a few issues: GLoom is a plugin-based web app manager written in Go (perhaps a pico-paas). GLoom's focus is to provide and simple and efficient way to host micro-web apps easily. Currently, GLoom is a fun little proof of concept, and now even supports unloading plugins, and gracefully handles plugins that crash, but it is not yet ready for production use, and may not ever be. GLoom is still in early development, so expect some rough edges and bugs and at its heart, GLoom is just a proof of concept, fun to write, and fun to use, but not production ready.
- Incorrectly confgiured plugins will cause GLoom to crash
- GLoom plugins are cannot be reloaded when they are updated
As far as I see it, these issues are unfixable currently, Go Plugins __cannot__ be unloaded, and there's no way to separate GLoom plugins from the host proces, thus meaning if a plugin crashes, GLoom will crash as well.
## Features ## Features
- Plugin-based architecture - Plugin-based architecture
- RPC-based communication between GLoom and plugins - RPC-based communication between GLoom and plugins
- Built-in plugin management system - Built-in plugin management system
- Built-in plugin management UI
## Getting Started ## Getting Started
@@ -24,23 +18,35 @@ As far as I see it, these issues are unfixable currently, Go Plugins __cannot__
### Installation ### Installation
1. Clone the repository: 1. Clone the repository:
```bash ```bash
git clone https://github.com/juls0730/gloom.git git clone https://github.com/juls0730/gloom.git
``` ```
2. Run the project: 2. Run the project:
```bash ```bash
zqdgr run zqdgr run
``` ```
or if you want to build the project: or if you want to build the project:
```bash ```bash
zqdgr build zqdgr build
``` ```
and if you want to build the project without the GLoom management Interface (you will not be able to manage plugins wunless you have another interface like GLoomI):
```bash
zqdgr build:no-gloomi
```
and make sure to set the `DISABLE_GLOOMI` environment variable to `true` in the `.env` file.
## Configuring
GLoom is configured using environment variables. The following environment variables are supported:
- `DEBUG` - Enables debug logging. This is a boolean value, so you can set it to any truthy value to enable debug logging.
- `DISABLE_GLOOMI` - Disables the GLoomI plugin. This is a boolean value, so you can set it to any truthy value to disable the GLoomI plugin.
- `PLUGINS_DIR` - The directory where plugins are stored. This is a string value, so you can set it to any directory path you want. The default value is `plugs`.
## Usage ## Usage
please read [DOCUMENTATION.md](DOCUMENTATION.md) please read [DOCUMENTATION.md](DOCUMENTATION.md)

View File

@@ -1,5 +1,7 @@
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 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 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0=
@@ -19,6 +21,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= 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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -29,6 +35,7 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
@@ -40,3 +47,5 @@ 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/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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -21,11 +21,9 @@ func (p *GLoomI) Init() (*fiber.Config, error) {
return nil, fmt.Errorf("failed to connect to Gloom RPC server: %w", err) return nil, fmt.Errorf("failed to connect to Gloom RPC server: %w", err)
} }
p.client = client p.client = client
return nil, nil return &fiber.Config{
} BodyLimit: 1024 * 1024 * 1024 * 5, // 5GB
}, nil
func (p *GLoomI) Name() string {
return "GLoomI"
} }
type PluginData struct { type PluginData struct {
@@ -95,12 +93,13 @@ func (p *GLoomI) RegisterRoutes(router fiber.Router) {
return c.Status(fiber.StatusInternalServerError).SendString("Failed to read plugin file: " + err.Error()) return c.Status(fiber.StatusInternalServerError).SendString("Failed to read plugin file: " + err.Error())
} }
err = p.client.Call("GloomRPC.UploadPlugin", pluginUploadStruct, nil) reply := new(string)
err = p.client.Call("GloomRPC.UploadPlugin", pluginUploadStruct, reply)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Failed to upload plugin: " + err.Error()) return c.Status(fiber.StatusInternalServerError).SendString("Failed to upload plugin: " + err.Error())
} }
return c.Status(fiber.StatusOK).SendString("Plugin uploaded successfully") return c.Status(fiber.StatusOK).SendString(*reply)
}) })
apiRouter.Delete("/plugins/:pluginName", func(c fiber.Ctx) error { apiRouter.Delete("/plugins/:pluginName", func(c fiber.Ctx) error {

27
go.mod
View File

@@ -2,27 +2,14 @@ module github.com/juls0730/gloom
go 1.23.4 go 1.23.4
require github.com/mattn/go-sqlite3 v1.14.24 require (
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.24
)
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gofiber/fiber/v3 v3.0.0-beta.4 // 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/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 github.com/mattn/go-isatty v0.0.20 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/tinylib/msgp v1.2.5 // indirect golang.org/x/sys v0.29.0 // 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
) )

45
go.sum
View File

@@ -1,46 +1,13 @@
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
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 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 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.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=

662
main.go
View File

@@ -1,48 +1,66 @@
package main package main
import ( import (
"bufio"
"context"
"database/sql" "database/sql"
"embed" "embed"
"fmt" "fmt"
"log/slog" "log/slog"
"math/rand/v2"
"net" "net"
"net/http"
"net/http/httputil"
"net/rpc" "net/rpc"
"net/url"
"os" "os"
"plugin" "os/exec"
"path"
"path/filepath"
"strconv"
"strings" "strings"
"sync"
"sync/atomic"
"time"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/logger"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/juls0730/gloom/libs" "github.com/juls0730/gloom/libs"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
//go:embed schema.sql //go:embed schema.sql host
var embeddedAssets embed.FS var embeddedAssets embed.FS
type Plugin interface { type PluginHost struct {
Init() (*fiber.Config, error) UnixSocket string
RegisterRoutes(app fiber.Router) Process *os.Process
Name() string Domains []string
}
type PluginInstance struct {
Plugin Plugin
Name string
Path string
Router *fiber.App
} }
type GLoom struct { type GLoom struct {
Plugins []PluginInstance // path to the pluginHost binary
domainMap libs.SyncMap[string, *PluginInstance] tmpDir string
pluginDir string
plugins libs.SyncMap[string, *PluginHost]
hostMap libs.SyncMap[string, bool]
DB *sql.DB DB *sql.DB
fiber *fiber.App ProxyManager *ProxyManager
} }
func NewGloom(app *fiber.App) (*GLoom, error) { func NewGloom(proxyManager *ProxyManager) (*GLoom, error) {
if err := os.MkdirAll("plugs", 0755); err != nil { pluginsDir := os.Getenv("PLUGINS_DIR")
if pluginsDir == "" {
pluginsDir = "plugs"
}
pluginsDir, err := filepath.Abs(pluginsDir)
if err != nil {
return nil, err
}
if err := os.MkdirAll(pluginsDir, 0755); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
panic(err) panic(err)
} }
@@ -63,18 +81,35 @@ func NewGloom(app *fiber.App) (*GLoom, error) {
return nil, err return nil, err
} }
pluginHost, err := embeddedAssets.ReadFile("host")
if err != nil {
return nil, err
}
tmpDir, err := os.MkdirTemp(os.TempDir(), "gloom")
if err != nil {
return nil, err
}
if err = os.WriteFile(tmpDir+"/pluginHost", pluginHost, 0755); err != nil {
return nil, err
}
slog.Debug("Wrote pluginHost", "dir", tmpDir+"/pluginHost")
gloom := &GLoom{ gloom := &GLoom{
Plugins: []PluginInstance{}, tmpDir: tmpDir,
domainMap: libs.SyncMap[string, *PluginInstance]{}, pluginDir: pluginsDir,
plugins: libs.SyncMap[string, *PluginHost]{},
DB: db, DB: db,
fiber: app, ProxyManager: proxyManager,
} }
return gloom, nil return gloom, nil
} }
func (gloom *GLoom) LoadInitialPlugins() error { func (gloom *GLoom) LoadInitialPlugins() error {
plugins, err := gloom.DB.Query("SELECT path, domains FROM plugins") slog.Debug("Loading initial plugins")
plugins, err := gloom.DB.Query("SELECT path, domains, name FROM plugins")
if err != nil { if err != nil {
return err return err
} }
@@ -84,15 +119,16 @@ func (gloom *GLoom) LoadInitialPlugins() error {
var plugin struct { var plugin struct {
Path string Path string
Domain string Domain string
Name string
} }
if err := plugins.Scan(&plugin.Path, &plugin.Domain); err != nil { if err := plugins.Scan(&plugin.Path, &plugin.Domain, &plugin.Name); err != nil {
return err return err
} }
domains := strings.Split(plugin.Domain, ",") domains := strings.Split(plugin.Domain, ",")
if err := gloom.RegisterPlugin(plugin.Path, domains); err != nil { if err := gloom.RegisterPlugin(plugin.Path, plugin.Name, domains); err != nil {
slog.Warn("Failed to register plugin", "pluginPath", plugin.Path, "error", err) slog.Warn("Failed to register plugin", "pluginPath", plugin.Path, "error", err)
} }
} }
@@ -100,59 +136,177 @@ func (gloom *GLoom) LoadInitialPlugins() error {
return nil return nil
} }
func (gloom *GLoom) RegisterPlugin(pluginPath string, domains []string) error { var ErrLocked = fmt.Errorf("item is locked")
type MutexLock[T comparable] struct {
mu sync.Mutex
deployed map[T]context.CancelFunc
}
func NewMutexLock[T comparable]() *MutexLock[T] {
return &MutexLock[T]{
deployed: make(map[T]context.CancelFunc),
}
}
func (dt *MutexLock[T]) Lock(id T, ctx context.Context) (context.Context, error) {
dt.mu.Lock()
defer dt.mu.Unlock()
// Check if the object is locked
if _, exists := dt.deployed[id]; exists {
slog.Debug("Item is locked", "id", id)
return nil, ErrLocked
}
// Create a context that can be cancelled
ctx, cancel := context.WithCancel(ctx)
// Store the cancel function
dt.deployed[id] = cancel
return ctx, nil
}
func (dt *MutexLock[T]) Unlock(id T) {
dt.mu.Lock()
defer dt.mu.Unlock()
// Remove the app from deployed tracking
if cancel, exists := dt.deployed[id]; exists {
// Cancel the context
cancel()
// Remove from map
delete(dt.deployed, id)
}
}
var deploymentLock = NewMutexLock[string]()
func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []string) (err error) {
slog.Info("Registering plugin", "pluginPath", pluginPath, "domains", domains) slog.Info("Registering plugin", "pluginPath", pluginPath, "domains", domains)
p, err := plugin.Open(pluginPath) pathStr := strconv.FormatUint(uint64(rand.Uint64()), 16)
socketPath := path.Join(gloom.tmpDir, pathStr+".sock")
controlPath := path.Join(gloom.tmpDir, pathStr+"-control.sock")
slog.Debug("Starting pluginHost", "pluginPath", pluginPath, "socketPath", socketPath)
processPath := path.Join(gloom.tmpDir, "pluginHost")
args := []string{pluginPath, socketPath, controlPath}
slog.Debug("Starting pluginHost", "args", args)
cmd := exec.Command(processPath, args...)
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start pluginHost: %w", err)
}
process := cmd.Process
for {
_, err := os.Stat(controlPath)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
conn, err := net.DialTimeout("unix", controlPath, 5*time.Second)
if err != nil {
_ = process.Signal(os.Interrupt)
return fmt.Errorf("failed to connect to plugin control socket: %w", err)
}
defer conn.Close()
reader := bufio.NewReader(conn)
readTimeout := time.After(30 * time.Second)
select {
case <-readTimeout:
_ = process.Signal(os.Interrupt)
return fmt.Errorf("timed out waiting for plugin status")
default:
status, err := reader.ReadString('\n')
if err != nil {
_ = process.Signal(os.Interrupt)
return fmt.Errorf("error reading plugin status: %w", err)
}
status = strings.TrimSpace(status)
if status == "ready" {
slog.Debug("PluginHost ported ready", "pluginPath", pluginPath)
break
} else if strings.HasPrefix(status, "Error: ") {
errorMessage := strings.TrimPrefix(status, "Error: ")
_ = process.Signal(os.Interrupt)
return fmt.Errorf("plugin reported error: %s", errorMessage)
} else {
_ = process.Signal(os.Interrupt)
return fmt.Errorf("received unknown status from plugin: %s", status)
}
}
proxy, err := NewDeploymentProxy(socketPath)
if err != nil { if err != nil {
return err return err
} }
symbol, err := p.Lookup("Plugin") var oldProxy *Proxy
if err != nil {
return err
}
pluginLib, ok := symbol.(Plugin)
if !ok {
return fmt.Errorf("plugin is not a Plugin")
}
fiberConfig, err := pluginLib.Init()
if err != nil {
return err
}
if fiberConfig == nil {
fiberConfig = &fiber.Config{}
}
router := fiber.New(*fiberConfig)
pluginLib.RegisterRoutes(router)
pluginInstance := PluginInstance{
Plugin: pluginLib,
Name: pluginLib.Name(),
Path: pluginPath,
Router: router,
}
gloom.Plugins = append(gloom.Plugins, pluginInstance)
pluginPtr := &gloom.Plugins[len(gloom.Plugins)-1]
for _, domain := range domains { for _, domain := range domains {
gloom.domainMap.Store(domain, pluginPtr) var ok bool
oldProxy, ok = gloom.ProxyManager.Load(domain)
// there can only be one in a set of domains. If a is the domains already attached to the proxy, and b is
// a superset of a, but the new members of b are not in any other set, then we can be sure there is just one
if ok {
break
} }
}
// this will replace the old proxy with a new one
for _, domain := range domains {
gloom.ProxyManager.AddProxy(domain, proxy)
}
plugHost := &PluginHost{
UnixSocket: socketPath,
Process: process,
Domains: domains,
}
gloom.plugins.Store(pluginPath, plugHost)
if oldProxy != nil {
go func() {
oldProxy.GracefulShutdown(nil)
}()
}
slog.Debug("Registered plugin", "pluginPath", pluginPath, "domains", domains)
return nil return nil
} }
func (gloom *GLoom) DeletePlugin(pluginName string) { // removes plugin from proxy and kills the process
gloom.domainMap.Range(func(domain string, plugin *PluginInstance) bool { func (gloom *GLoom) DeletePlugin(pluginName string) error {
if plugin.Name == pluginName { slog.Debug("Deleting plugin", "pluginName", pluginName)
gloom.domainMap.Delete(domain)
plug, ok := gloom.plugins.Load(pluginName)
if !ok {
return fmt.Errorf("plugin not found")
} }
return true
}) for _, domain := range plug.Domains {
gloom.ProxyManager.RemoveDeployment(domain)
gloom.hostMap.Store(domain, false)
}
plug.Process.Signal(os.Interrupt)
for _, domain := range plug.Domains {
gloom.ProxyManager.RemoveDeployment(domain)
}
gloom.plugins.Delete(pluginName)
return nil
} }
func (gloom *GLoom) StartRPCServer() error { func (gloom *GLoom) StartRPCServer() error {
@@ -192,23 +346,17 @@ type PluginData struct {
} }
func (rpc *GloomRPC) ListPlugins(_ struct{}, reply *[]PluginData) error { func (rpc *GloomRPC) ListPlugins(_ struct{}, reply *[]PluginData) error {
var plugins []PluginData = make([]PluginData, 0) var pluginsArray []PluginData = make([]PluginData, 0, len(rpc.gloom.plugins.Keys()))
var domains map[string][]string = make(map[string][]string) rpc.gloom.plugins.Range(func(key string, value *PluginHost) (shouldContinue bool) {
pluginData := PluginData{
rpc.gloom.domainMap.Range(func(domain string, plugin *PluginInstance) bool { Name: key,
domains[plugin.Name] = append(domains[plugin.Name], domain) Domains: value.Domains,
}
pluginsArray = append(pluginsArray, pluginData)
return true return true
}) })
for _, plugin := range rpc.gloom.Plugins { *reply = pluginsArray
var pluginDataStruct PluginData
pluginDataStruct.Name = plugin.Name
pluginDataStruct.Domains = domains[plugin.Name]
plugins = append(plugins, pluginDataStruct)
}
*reply = plugins
return nil return nil
} }
@@ -219,85 +367,143 @@ type PluginUpload struct {
} }
func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error { func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error {
_, err := deploymentLock.Lock(plugin.Name, context.Background())
if err != nil && err == ErrLocked {
*reply = "Plugin is already being updated"
return fmt.Errorf("plugin is already being updated")
}
defer deploymentLock.Unlock(plugin.Name)
slog.Info("Uploading plugin", "plugin", plugin.Name, "domains", plugin.Domains) slog.Info("Uploading plugin", "plugin", plugin.Name, "domains", plugin.Domains)
pluginPath, err := filepath.Abs(fmt.Sprintf("plugs/%s", plugin.Name))
if err != nil {
*reply = "Plugin upload failed"
return err
}
var plugExists bool var plugExists bool
rpc.gloom.DB.QueryRow("SELECT path FROM plugins WHERE path = ?", "plugs/"+plugin.Name).Scan(&plugExists) // TODO: make name a consistent identifier
slog.Debug("Checking if plugin exists", "pluginPath", pluginPath, "pluginName", plugin.Name)
rpc.gloom.DB.QueryRow("SELECT 1 FROM plugins WHERE name = ?", plugin.Name).Scan(&plugExists)
slog.Debug("Plugin exists", "pluginExists", plugExists)
var domains []string var domains []string
var newDomains []string
if plugExists { if plugExists {
// if plugin exists, we need to not check for domains that this plug has already registered, but instead check for new domains this plugin is registering // if plugin exists, we need to not check for domains that this plug has already registered, but instead check for new domains this plugin is registering
domains = make([]string, 0) domainsMap := map[string]bool{}
var existingDomains []string newDomains = make([]string, 0)
err := rpc.gloom.DB.QueryRow("SELECT domains FROM plugins WHERE path = ?", "plugs/"+plugin.Name).Scan(&existingDomains) removedDomains := make([]string, 0)
var sqlDomains string
err := rpc.gloom.DB.QueryRow("SELECT domains FROM plugins WHERE name = ?", plugin.Name).Scan(&sqlDomains)
if err != nil { if err != nil {
return err return err
} }
for _, domain := range existingDomains { // domains that are already related to the plugin
var found bool existingDomains := strings.Split(sqlDomains, ",")
for _, domainToCheck := range plugin.Domains {
if domain == domainToCheck { for _, domain := range plugin.Domains {
found = true domainsMap[domain] = true
break
}
}
if !found {
domains = append(domains, domain)
} }
found = false for _, domain := range existingDomains {
if _, ok := domainsMap[domain]; !ok {
removedDomains = append(removedDomains, domain)
}
}
for _, domain := range removedDomains {
slog.Debug("Removing domain from plugin", "domain", domain, "plugin", plugin.Name)
rpc.gloom.ProxyManager.RemoveDeployment(domain)
}
for domain := range domainsMap {
if exists, _ := rpc.gloom.hostMap.Load(domain); !exists {
newDomains = append(newDomains, domain)
}
slog.Debug("Adding domain to plugin", "domain", domain, "plugin", plugin.Name)
domains = append(domains, domain)
} }
} else { } else {
domains = plugin.Domains domains = plugin.Domains
newDomains = plugin.Domains
} }
for _, domain := range domains { for _, domain := range newDomains {
_, ok := rpc.gloom.domainMap.Load(domain) _, ok := rpc.gloom.hostMap.Load(domain)
if ok { if ok {
*reply = fmt.Sprintf("Domain %s already exists", domain) *reply = fmt.Sprintf("Domain %s already exists", domain)
return nil return nil
} }
} }
plugsDir := "plugs"
if os.Getenv("PLUGINS_DIR") != "" {
plugsDir = os.Getenv("PLUGINS_DIR")
}
if _, err := os.Stat(plugsDir); os.IsNotExist(err) {
if err := os.Mkdir(plugsDir, 0755); err != nil {
*reply = "Plugin upload failed"
return err
}
}
// regardless of if plugin exists or not, we'll upload the file since this could be an update to an existing plugin // regardless of if plugin exists or not, we'll upload the file since this could be an update to an existing plugin
if err := os.WriteFile(fmt.Sprintf("plugs/%s", plugin.Name), plugin.Data, 0644); err != nil { if err := os.WriteFile(pluginPath, plugin.Data, 0644); err != nil {
*reply = "Plugin upload failed"
return err return err
} }
fmt.Println("Plugin uploaded successfully") fmt.Println("Plugin uploaded successfully")
if err := rpc.gloom.RegisterPlugin(pluginPath, plugin.Name, domains); err != nil {
os.Remove(pluginPath)
slog.Warn("Failed to register uplaoded plguin", "pluginPath", pluginPath, "error", err)
*reply = fmt.Sprintf("Plugin upload failed: %v", err)
return err
}
if !plugExists {
_, err = rpc.gloom.DB.Exec("INSERT INTO plugins (path, name, domains) VALUES (?, ?, ?)", pluginPath, plugin.Name, strings.Join(plugin.Domains, ","))
if err != nil {
*reply = fmt.Sprintf("Plugin upload failed: %v", err)
return err
}
} else {
_, err = rpc.gloom.DB.Exec("UPDATE plugins SET domains = ?, path = ? WHERE name = ?", strings.Join(plugin.Domains, ","), pluginPath, plugin.Name)
if err != nil {
*reply = fmt.Sprintf("Plugin upload failed: %v", err)
return err
}
}
if plugExists { if plugExists {
// exit out early otherwise we risk creating multiple of the same plugin and causing undefined behavior // exit out early otherwise we risk creating multiple of the same plugin and causing undefined behavior
*reply = "Plugin updated successfully" *reply = "Plugin updated successfully"
return nil return nil
} }
if err := rpc.gloom.RegisterPlugin("plugs/"+plugin.Name, plugin.Domains); err != nil {
slog.Warn("Failed to register uplaoded plguin", "pluginPath", "plugs/"+plugin.Name, "error", err)
*reply = "Plugin upload failed"
return nil
}
rpc.gloom.DB.Exec("INSERT INTO plugins (path, domains) VALUES (?, ?)", "plugs/"+plugin.Name, strings.Join(plugin.Domains, ","))
*reply = "Plugin uploaded successfully" *reply = "Plugin uploaded successfully"
return nil return nil
} }
func (rpc *GloomRPC) DeletePlugin(pluginName string, reply *string) error { func (rpc *GloomRPC) DeletePlugin(pluginName string, reply *string) error {
var targetPlugin PluginInstance if pluginName == "GLoomI" {
for _, plugin := range rpc.gloom.Plugins { *reply = "GLoomI cannot be deleted since it is not a plugin that is loaded by a user. If you wish to disable GLoomI, set DISABLE_GLOOMI=true in your .env file"
if plugin.Name == pluginName { return nil
targetPlugin = plugin
break
}
} }
_, err := rpc.gloom.DB.Exec("DELETE FROM plugins WHERE path = ?", targetPlugin.Path) _, ok := rpc.gloom.plugins.Load(pluginName)
if err != nil { if !ok {
*reply = "Plugin not found" *reply = "Plugin not found"
return err return nil
} }
err = os.Remove(targetPlugin.Path) _, err := rpc.gloom.DB.Exec("DELETE FROM plugins WHERE name = ?", pluginName)
if err != nil { if err != nil {
*reply = "Plugin not found" *reply = "Plugin not found"
return err return err
@@ -316,54 +522,230 @@ func init() {
} }
func main() { func main() {
app := fiber.New(fiber.Config{ debug, err := strconv.ParseBool(os.Getenv("DEBUG"))
BodyLimit: 1024 * 1024 * 1024 * 5, // 5GB if err != nil {
}) debug = false
}
app.Use(logger.New(logger.Config{ level := slog.LevelInfo
CustomTags: map[string]logger.LogFunc{ if debug {
"app": func(output logger.Buffer, c fiber.Ctx, data *logger.Data, extraParam string) (int, error) { level = slog.LevelDebug
output.WriteString(c.Host()) }
return len(output.Bytes()), nil
},
},
Format: " ${time} | ${status} | ${latency} | ${ip} | ${method} | ${app} | ${path}\n",
}))
gloom, err := NewGloom(app) logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
slog.SetDefault(logger)
proxyManager := NewProxyManager()
gloom, err := NewGloom(proxyManager)
if err != nil { if err != nil {
panic(err) 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 { if err := gloom.StartRPCServer(); err != nil {
panic("Failed to start RPC server: " + err.Error()) panic("Failed to start RPC server: " + err.Error())
} }
gloom.LoadInitialPlugins() gloom.LoadInitialPlugins()
if os.Getenv("DISABLE_GLOOMI") != "true" { enableGloomi, err := strconv.ParseBool(os.Getenv("ENABLE_GLOOMI"))
if err != nil {
enableGloomi = true
}
if enableGloomi {
hostname := os.Getenv("GLOOMI_HOSTNAME") hostname := os.Getenv("GLOOMI_HOSTNAME")
if hostname == "" { if hostname == "" {
hostname = "127.0.0.1" hostname = "127.0.0.1"
} }
if err := gloom.RegisterPlugin("plugs/gloomi.so", []string{hostname}); err != nil { if err := gloom.RegisterPlugin("plugs/gloomi.so", "GLoomI", []string{hostname}); err != nil {
panic("Failed to register GLoomI: " + err.Error()) panic("Failed to register GLoomI: " + err.Error())
} }
} }
fmt.Println("Server running at http://localhost:3000") fmt.Println("Server running at http://localhost:3000")
if err := app.Listen(":3000"); err != nil { if err := gloom.ProxyManager.ListenAndServe("127.0.0.1:3000"); err != nil {
panic(err) panic(err)
} }
} }
// this is the object that oversees the proxying of requests to the correct deployment
type ProxyManager struct {
libs.SyncMap[string, *Proxy]
}
func NewProxyManager() *ProxyManager {
return &ProxyManager{}
}
func (proxyManager *ProxyManager) ListenAndServe(host string) error {
slog.Info("Proxy server starting", "url", host)
if err := http.ListenAndServe(host, proxyManager); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("failed to start proxy server: %v", err)
}
return nil
}
// Stops forwarding traffic to a deployment
func (proxyManager *ProxyManager) RemoveDeployment(host string) {
slog.Info("Removing proxy", "host", host)
proxyManager.Delete(host)
}
// Starts forwarding traffic to a deployment. The deployment must be ready to recieve requests before this is called.
func (proxyManager *ProxyManager) AddProxy(host string, proxy *Proxy) {
slog.Debug("Adding proxy", "host", host)
proxyManager.Store(host, proxy)
}
// This function is responsible for taking an http request and forwarding it to the correct deployment
func (proxyManager *ProxyManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
host := r.Host
path := r.URL.Path
method := r.Method
ip := getClientIP(r)
slog.Debug("Proxying request", "host", host, "path", path, "method", method, "ip", ip)
proxy, ok := proxyManager.Load(host)
if !ok {
http.Error(w, "Not found", http.StatusNotFound)
logRequest(host, http.StatusNotFound, time.Since(start), ip, method, path)
return
}
// Create a custom ResponseWriter to capture the status code
rw := &ResponseWriterInterceptor{ResponseWriter: w, statusCode: http.StatusOK}
proxy.proxyFunc.ServeHTTP(rw, r)
latency := time.Since(start)
statusCode := rw.statusCode
logRequest(host, statusCode, latency, ip, method, path)
}
// getClientIP retrieves the client's IP address from the request.
// It handles cases where the IP might be forwarded by proxies.
func getClientIP(r *http.Request) string {
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
return forwarded
}
return r.RemoteAddr
}
// ResponseWriterInterceptor is a custom http.ResponseWriter that captures the status code.
type ResponseWriterInterceptor struct {
http.ResponseWriter
statusCode int
}
func (rw *ResponseWriterInterceptor) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func logRequest(app string, status int, latency time.Duration, ip, method, path string) {
slog.Info("Proxy Request",
slog.String("time", time.Now().Format(time.RFC3339)),
slog.Int("status", status),
slog.Duration("latency", latency),
slog.String("ip", ip),
slog.String("method", method),
slog.String("app", app),
slog.String("path", path),
)
}
type unixDialer struct {
socketPath string
}
// dialContext implements DialContext but ignored everthing and just gives you a connection to the unix socket
func (d *unixDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return net.Dial("unix", d.socketPath)
}
func NewUnixSocketTransport(socketPath string) *http.Transport {
return &http.Transport{
DialContext: (&unixDialer{socketPath: socketPath}).DialContext,
}
}
type Proxy struct {
socket string
proxyFunc *httputil.ReverseProxy
shutdownTimeout time.Duration
activeRequests int64
}
const PROXY_SHUTDOWN_TIMEOUT = 30 * time.Second
// Creates a proxy for a given deployment
func NewDeploymentProxy(socket string) (*Proxy, error) {
proxy := &Proxy{
socket: socket,
shutdownTimeout: PROXY_SHUTDOWN_TIMEOUT,
activeRequests: 0,
}
transport := &http.Transport{
DialContext: (&unixDialer{socketPath: socket}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
MaxIdleConnsPerHost: 100,
ForceAttemptHTTP2: false,
}
proxy.proxyFunc = &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL = &url.URL{
Scheme: "http",
Host: req.Host,
Path: req.URL.Path,
}
atomic.AddInt64(&proxy.activeRequests, 1)
},
Transport: transport,
ModifyResponse: func(resp *http.Response) error {
atomic.AddInt64(&proxy.activeRequests, -1)
return nil
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
slog.Error("Proxy error", "error", err)
atomic.AddInt64(&proxy.activeRequests, -1)
w.WriteHeader(http.StatusInternalServerError)
},
}
return proxy, nil
}
func (p *Proxy) GracefulShutdown(shutdownFunc func()) {
slog.Debug("Shutting down proxy", "socket", p.socket)
ctx, cancel := context.WithTimeout(context.Background(), p.shutdownTimeout)
defer cancel()
done := false
for !done {
select {
case <-ctx.Done():
slog.Debug("Proxy shutdown timed out", "socket", p.socket)
done = true
default:
if atomic.LoadInt64(&p.activeRequests) == 0 {
slog.Debug("Proxy shutdown completed successfully", "socket", p.socket)
done = true
}
time.Sleep(time.Second)
}
}
if shutdownFunc != nil {
shutdownFunc()
}
}

View File

@@ -2,10 +2,11 @@ module github.com/juls0730/gloom-plugin
go 1.23.4 go 1.23.4
require github.com/gofiber/fiber/v3 v3.0.0-beta.4
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
github.com/gofiber/fiber/v3 v3.0.0-beta.4 // indirect
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

View File

@@ -1,5 +1,7 @@
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 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 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0=
@@ -19,6 +21,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= 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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -29,6 +35,7 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
@@ -40,3 +47,5 @@ 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/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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -8,13 +8,9 @@ func (p *MyPlugin) Init() (*fiber.Config, error) {
return nil, nil return nil, nil
} }
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 { router.Get("/", func(c fiber.Ctx) error {
return c.Status(fiber.StatusTeapot).SendString("Welcome to MyPlugin!") return c.Status(fiber.StatusOK).SendString("Welcome to MyPlugin!")
}) })
router.Get("/hello", func(c fiber.Ctx) error { router.Get("/hello", func(c fiber.Ctx) error {

26
pluginHost/README.md Normal file
View File

@@ -0,0 +1,26 @@
# Plugin Host
This is the plugin host for GLoom. This is a small program that is responsible for loading and managing plugins. It is responsible for starting the plugin and forwarding requests to it. This is meant to be used with GLoom, but can be used as a standalone program if you so choose. The Plugin Host is built automatically when you build GLoom via `zqdgr build`.
## Building
To build the plugin host standalone, run the following command in the `pluginHost` directory:
```bash
zqdgr build
```
or run the following command in the project root:
```bash
zqdgr build:pluginHost
```
## Running
To run the plugin host, run the following command:
```bash
./host <pluginPath> <socketPath> [controlPath]
```
- `pluginPath` - The path to the plugin to load.
- `socketPath` - The path to the socket that the plugin will use to listen for http requests through.
- `controlPath` - (Optional) The path to the control socket. If not provided, the host will not send errors or status messages to the control socket and instead log them to stdout and stderr.

26
pluginHost/go.mod Normal file
View File

@@ -0,0 +1,26 @@
module github.com/juls0730/gloom/pluginHost
go 1.24.2
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
)

51
pluginHost/go.sum Normal file
View File

@@ -0,0 +1,51 @@
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
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=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

145
pluginHost/main.go Normal file
View File

@@ -0,0 +1,145 @@
package main
import (
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"path/filepath"
"plugin"
"github.com/gofiber/fiber/v3"
)
var pluginPath string
var socketPath string
var controlPath string
func init() {
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "Usage: pluginHost <pluginPath> <socketPath>")
os.Exit(1)
}
pluginPath = os.Args[1]
socketPath = os.Args[2]
if len(os.Args) > 3 {
controlPath = os.Args[3]
}
}
type Plugin interface {
Init() (*fiber.Config, error)
RegisterRoutes(app fiber.Router)
// Name() string
}
type PluginInstance struct {
Plugin Plugin
Name string
Path string
Router *fiber.App
}
// Init is the entry point for a container process
func (p *PluginInstance) Run(pluginName string) {
log.Printf("Starting container with plugin %s", pluginName)
// Load and initialize the plugin here
}
func main() {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
go func() {
<-signalChan
// TODO: maybe do something graceful here
fmt.Println("Received SIGINT, shutting down...")
os.Exit(0)
}()
var writer io.Writer
writer = os.Stderr
if controlPath != "" {
fmt.Printf("Waiting for control connection on %s\n", controlPath)
controlListener, err := net.Listen("unix", controlPath)
if err != nil {
log.Fatalf("Error listening on control socket: %v", err)
}
defer controlListener.Close()
conn, err := controlListener.Accept()
if err != nil {
log.Printf("Error accepting control connection: %v", err)
return
}
defer conn.Close()
var ok bool
writer, ok = conn.(io.Writer)
if !ok {
log.Printf("Control connection is not a writer")
return
}
}
if _, err := os.Stat(socketPath); err == nil {
fmt.Fprintf(writer, "Error: Socket %s already exists\n", socketPath)
os.Exit(1)
}
realPluginPath, err := filepath.Abs(pluginPath)
if err != nil {
fmt.Fprintf(writer, "Error: could not get absolute plugin path: %v\n", err)
os.Exit(1)
}
p, err := plugin.Open(realPluginPath)
if err != nil {
fmt.Fprintf(writer, "Error: could not open plugin %s: %v\n", realPluginPath, err)
os.Exit(1)
}
symbol, err := p.Lookup("Plugin")
if err != nil {
fmt.Fprintf(writer, "Error: could not find 'Plugin' symbol in %s: %v\n", realPluginPath, err)
os.Exit(1)
}
pluginLib, ok := symbol.(Plugin)
if !ok {
fmt.Fprintf(writer, "Error: symbol 'Plugin' in %s is not a Plugin interface\n", realPluginPath)
os.Exit(1)
}
pluginConfig, err := pluginLib.Init()
if err != nil {
fmt.Fprintf(writer, "Error: error initializing plugin %s: %v\n", realPluginPath, err)
os.Exit(1)
}
config := fiber.Config{}
if pluginConfig != nil {
config = *pluginConfig
}
router := fiber.New(config)
pluginLib.RegisterRoutes(router)
// listen for connections on the socket
listener, err := net.Listen("unix", socketPath)
if err != nil {
fmt.Fprintf(writer, "Error: error listening on socket %s: %v\n", socketPath, err)
os.Exit(1)
}
fmt.Fprintf(writer, "ready\n")
// technically this can still error
router.Listener(listener, fiber.ListenConfig{
DisableStartupMessage: true,
})
}

View File

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

View File

@@ -1,12 +1,16 @@
{ {
"name": "Go Project", "name": "GLoom",
"version": "0.0.1", "version": "0.0.1",
"description": "Example description", "description": "GLoom is a plugin-based web app manager",
"author": "you", "author": "juls0730",
"license": "BSL-1.0", "license": "MIT",
"scripts": { "scripts": {
"build": "sh -c \"cd gloomi; zqdgr build\" && go build", "build": "zqdgr build:gloomi && zqdgr build:pluginHost && go build",
"dev": "sh -c \"cd gloomi; zqdgr build\" && go run main.go" "build:pluginHost": "sh -c \"cd pluginHost; go build -o ../host main.go\"",
"build:gloomi": "sh -c \"cd gloomi; zqdgr build\"",
"build:no-gloomi": "go build",
"clean": "rm -rf plugs && rm -rf host && rm -rf gloom.db && rm -rf plugin/plugin.so && rm -rf gloom",
"dev": "zqdgr build && ./gloom"
}, },
"pattern": "**/*.go", "pattern": "**/*.go",
"excluded_dirs": [] "excluded_dirs": []