properly organize and configure gloom

This commit is contained in:
Zoe
2025-05-16 17:23:32 +00:00
parent a6f4782518
commit a8ec911f74
12 changed files with 246 additions and 90 deletions

View File

@@ -1,2 +0,0 @@
PRELOAD_PLUGINS='[{"file": "gloomi.so", "domains": ["localhost"]}]' # make sure gloomi.so is in the $PLUGINS_DIR
PLUGINS_DIR=plugs

6
.gitignore vendored
View File

@@ -1,6 +1,4 @@
plugs plugs
gloom dist
host
**/*.so **/*.so
.env gloomd
gloom.db

View File

@@ -39,19 +39,33 @@ This project is primarily written for Linux, and has only been tested on Linux,
zqdgr build:gloom zqdgr build:gloom
``` ```
and make sure to clear the `PRELOAD_PLUGINS` environment variable and set it to your preferred management interface, or nothing at all if you don't want to use a management interface. This will give you the same standalone binary that you would get if you ran `zqdgr build`, but without the GLoom
management interface and with a blank config file.
## Configuring ## Configuring
GLoom is configured using environment variables. The following environment variables are supported: GLoom is primarily configured through the config.toml file, but there are a few environment variables that can be used
to confgiure GLoom. 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.
- `GLOOM_DIR` - The directory where GLoom stores its data. plugins will be stored in this directory, the GLoom config
will be stored in this directory, and the pluginHost binary will be unpacked into this directory. the default value is
`$XDG_DATA_HOME/gloom` if it exists, otherwise `$HOME/.local/share/gloom`.
GLoom will also use a config file in the `GLOOM_DIR` to configure plugins and other settings. The config file is a toml file, and the default config is:
```toml
[[plugins]]
file = "gloomi.so"
domains = ["localhost"]
```
The `[[plugins]]` array is a list of plugins to preload. Each plugin is an object with the following fields:
- `file` - The path to the plugin file. This is a string value.
- `domains` - An array of domains to forward requests to this plugin. This is an array of strings.
- `DEBUG` - Enables debug logging. This is a boolean value, so you can set it to any truthy value to enable debug logging.
- `PRELOAD_PLUGINS` - A json array of plugins to preload. The default value of this is `gloomi`, this is how GLoomI is loaded by default, and how replacement interfaces can be loaded. The format is in json, and the default value is:
```json
[{"file": "gloomi.so", "domains": ["localhost"]}]
```
It is not recommended to preload any plugin other than a management interface, this is because preloaded plugins are protected and cannot be updated in an green-blue fashion.
- `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

15
embed_default.go Normal file
View File

@@ -0,0 +1,15 @@
//go:build gloomi
// +build gloomi
package main
import "embed"
//go:embed schema.sql dist/host dist/gloomi.so
var embeddedAssets embed.FS
var defaultConfig = []byte(`
[[plugins]]
file = "gloomi.so"
domains = ["localhost"]
`)

11
embed_nogloomi.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build !gloomi
// +build !gloomi
package main
import "embed"
//go:embed schema.sql dist/host
var embeddedAssets embed.FS
var defaultConfig = []byte("# no plugins to preload in this build\n")

View File

@@ -5,7 +5,7 @@
"author": "juls0730", "author": "juls0730",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "go build -buildmode=plugin -o ../plugs/gloomi.so main.go" "build": "go build -ldflags '-w -s' -buildmode=plugin -o ../dist/gloomi.so main.go"
}, },
"pattern": "**/*.go", "pattern": "**/*.go",
"excluded_dirs": [] "excluded_dirs": []

5
go.mod
View File

@@ -8,4 +8,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.24 github.com/mattn/go-sqlite3 v1.14.24
) )
require golang.org/x/sync v0.14.0 // indirect require (
github.com/BurntSushi/toml v1.5.0
golang.org/x/sync v0.14.0 // indirect
)

2
go.sum
View File

@@ -1,3 +1,5 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/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/juls0730/sentinel v0.0.0-20250515154110-2e7e6586cacd h1:JNazPdlAs307Gtaqmb+wfCjcOzO3MRXxg9nf0bAKAnc= github.com/juls0730/sentinel v0.0.0-20250515154110-2e7e6586cacd h1:JNazPdlAs307Gtaqmb+wfCjcOzO3MRXxg9nf0bAKAnc=

182
main.go
View File

@@ -4,8 +4,6 @@ import (
"bufio" "bufio"
"context" "context"
"database/sql" "database/sql"
"embed"
"encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"math/rand/v2" "math/rand/v2"
@@ -21,34 +19,39 @@ import (
"sync" "sync"
"time" "time"
"github.com/BurntSushi/toml"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/juls0730/gloom/libs" "github.com/juls0730/gloom/libs"
"github.com/juls0730/sentinel" "github.com/juls0730/sentinel"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
//go:embed schema.sql host
var embeddedAssets embed.FS
type PluginHost struct { type PluginHost struct {
UnixSocket string UnixSocket string
Process *os.Process Process *os.Process
Domains []string Domains []string
} }
type PluginConfig struct {
// TODO
}
type PreloadPlugin struct { type PreloadPlugin struct {
File string `json:"file"` File string `json:"file"`
Domains []string `json:"domains"` Domains []string `json:"domains"`
} }
type GLoom struct { type GLoom struct {
// path to the pluginHost binary // path to the /tmp directory where the pluginHost binary is unpacked, and where the pluginHost sockets are created
tmpDir string tmpDir string
gloomDir string
pluginDir string pluginDir string
preloadPlugins []PreloadPlugin preloadPlugins []PreloadPlugin
// maps plugin names to plugins
plugins libs.SyncMap[string, *PluginHost] plugins libs.SyncMap[string, *PluginHost]
// maps domain names to whether or not they are currently being forwarded to
hostMap libs.SyncMap[string, bool] hostMap libs.SyncMap[string, bool]
DB *sql.DB DB *sql.DB
@@ -56,23 +59,27 @@ type GLoom struct {
} }
func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) { func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
pluginsDir := os.Getenv("PLUGINS_DIR") gloomDir := os.Getenv("GLOOM_DIR")
if pluginsDir == "" { if gloomDir == "" {
pluginsDir = "plugs" if os.Getenv("XDG_DATA_HOME") != "" {
gloomDir = filepath.Join(os.Getenv("XDG_DATA_HOME"), "gloom")
} else {
gloomDir = filepath.Join(os.Getenv("HOME"), ".local/share/gloom")
}
} }
pluginsDir, err := filepath.Abs(pluginsDir) gloomDir, err := filepath.Abs(gloomDir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := os.MkdirAll(pluginsDir, 0755); err != nil { if err := os.MkdirAll(gloomDir, 0755); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
panic(err) panic(err)
} }
} }
db, err := sql.Open("sqlite3", "gloom.db") db, err := sql.Open("sqlite3", filepath.Join(gloomDir, "gloom.db"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -87,48 +94,112 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
return nil, err return nil, err
} }
pluginHost, err := embeddedAssets.ReadFile("host") pluginHost, err := embeddedAssets.ReadFile("dist/host")
if err != nil { if err != nil {
return nil, err return nil, err
} }
pluginDir := filepath.Join(gloomDir, "plugs")
if err := os.MkdirAll(pluginDir, 0755); err != nil {
return nil, err
}
// if gloomi is built into the binary
if _, err := embeddedAssets.Open("dist/gloomi.so"); err == nil {
// and if the plugin doesn't exist, copy it over
if _, err := os.Stat(filepath.Join(pluginDir, "gloomi.so")); os.IsNotExist(err) {
gloomiData, err := embeddedAssets.ReadFile("dist/gloomi.so")
if err != nil {
return nil, err
}
if err := os.WriteFile(filepath.Join(pluginDir, "gloomi.so"), gloomiData, 0755); err != nil {
return nil, err
}
}
}
tmpDir, err := os.MkdirTemp(os.TempDir(), "gloom") tmpDir, err := os.MkdirTemp(os.TempDir(), "gloom")
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = os.WriteFile(tmpDir+"/pluginHost", pluginHost, 0755); err != nil {
return nil, err
}
slog.Debug("Wrote pluginHost", "dir", tmpDir+"/pluginHost")
var preloadPlugins []PreloadPlugin if _, err := os.Stat(filepath.Join(gloomDir, "pluginHost")); os.IsNotExist(err) {
preloadPluginsEnv, ok := os.LookupEnv("PRELOAD_PLUGINS") if err := os.WriteFile(filepath.Join(gloomDir, "pluginHost"), pluginHost, 0755); err != nil {
if ok { return nil, err
err = json.Unmarshal([]byte(preloadPluginsEnv), &preloadPlugins)
if err != nil {
panic(err)
}
} else {
preloadPlugins = []PreloadPlugin{
{
File: "gloomi.so",
Domains: []string{"localhost"},
},
} }
slog.Debug("Wrote pluginHost", "dir", filepath.Join(gloomDir, "pluginHost"))
} }
gloom := &GLoom{ gloom := &GLoom{
tmpDir: tmpDir, tmpDir: tmpDir,
pluginDir: pluginsDir, gloomDir: gloomDir,
preloadPlugins: preloadPlugins, pluginDir: pluginDir,
plugins: libs.SyncMap[string, *PluginHost]{}, plugins: libs.SyncMap[string, *PluginHost]{},
DB: db, DB: db,
ProxyManager: proxyManager, ProxyManager: proxyManager,
}
if err := gloom.loadConfig(); err != nil {
return nil, err
} }
return gloom, nil return gloom, nil
} }
func (gloom *GLoom) loadConfig() error {
configPath := filepath.Join(gloom.gloomDir, "config.toml")
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// no config file, write default config
if err := os.WriteFile(configPath, defaultConfig, 0644); err != nil {
return nil
}
}
configBytes, err := os.ReadFile(configPath)
if err != nil {
return err
}
var config any
_, err = toml.Decode(string(configBytes), &config)
if err != nil {
return err
}
proloadPlugins, ok := config.(map[string]any)["plugins"].([]map[string]any)
if ok {
for _, plugin := range proloadPlugins {
file, ok := plugin["file"].(string)
if !ok {
return fmt.Errorf("plugin file is not a string")
}
_, ok = plugin["domains"].([]any)
if !ok {
return fmt.Errorf("plugin domains is not an array")
}
domains := make([]string, 0, len(plugin["domains"].([]any)))
for _, domain := range plugin["domains"].([]any) {
if _, ok := domain.(string); !ok {
return fmt.Errorf("plugin domain is not a string")
}
domains = append(domains, domain.(string))
}
gloom.preloadPlugins = append(gloom.preloadPlugins, PreloadPlugin{
File: file,
Domains: domains,
})
}
}
return nil
}
func (gloom *GLoom) LoadInitialPlugins() error { func (gloom *GLoom) LoadInitialPlugins() error {
slog.Info("Loading initial plugins") slog.Info("Loading initial plugins")
@@ -221,11 +292,10 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []str
socketPath := path.Join(gloom.tmpDir, pathStr+".sock") socketPath := path.Join(gloom.tmpDir, pathStr+".sock")
controlPath := path.Join(gloom.tmpDir, pathStr+"-control.sock") controlPath := path.Join(gloom.tmpDir, pathStr+"-control.sock")
slog.Debug("Starting pluginHost", "pluginPath", pluginPath, "socketPath", socketPath) slog.Debug("Starting pluginHost", "pluginPath", pluginPath, "socketPath", socketPath, "controlPath", controlPath)
processPath := path.Join(gloom.tmpDir, "pluginHost") processPath := path.Join(gloom.gloomDir, "pluginHost")
args := []string{pluginPath, socketPath, controlPath} args := []string{pluginPath, socketPath, controlPath}
slog.Debug("Starting pluginHost", "args", args)
cmd := exec.Command(processPath, args...) cmd := exec.Command(processPath, args...)
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
@@ -338,23 +408,36 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []str
func (gloom *GLoom) DeletePlugin(pluginName string) error { func (gloom *GLoom) DeletePlugin(pluginName string) error {
slog.Debug("Deleting plugin", "pluginName", pluginName) slog.Debug("Deleting plugin", "pluginName", pluginName)
plug, ok := gloom.plugins.Load(pluginName) pluginHost, ok := gloom.plugins.Load(pluginName)
if !ok { if !ok {
return fmt.Errorf("plugin not found") return fmt.Errorf("plugin not found")
} }
for _, domain := range plug.Domains { for _, domain := range pluginHost.Domains {
gloom.ProxyManager.RemoveDeployment(domain) gloom.ProxyManager.RemoveDeployment(domain)
gloom.hostMap.Store(domain, false) gloom.hostMap.Store(domain, false)
} }
plug.Process.Signal(os.Interrupt) pluginHost.Process.Signal(os.Interrupt)
for _, domain := range plug.Domains { for _, domain := range pluginHost.Domains {
gloom.ProxyManager.RemoveDeployment(domain) gloom.ProxyManager.RemoveDeployment(domain)
} }
gloom.plugins.Delete(pluginName) gloom.plugins.Delete(pluginName)
var pluginPath string
if err := gloom.DB.QueryRow("SELECT path FROM plugins WHERE name = ?", pluginName).Scan(&pluginPath); err != nil {
return fmt.Errorf("failed to get plugin path: %w", err)
}
if _, err := gloom.DB.Exec("DELETE FROM plugins WHERE name = ?", pluginName); err != nil {
return fmt.Errorf("failed to delete plugin from database: %w", err)
}
if err := os.Remove(pluginPath); err != nil {
return fmt.Errorf("failed to remove plugin: %w", err)
}
return nil return nil
} }
@@ -450,7 +533,7 @@ func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error {
defer deploymentLock.Unlock(plugin.Name) 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)) pluginPath, err := filepath.Abs(filepath.Join(rpc.gloom.pluginDir, (plugin.Name + ".so")))
if err != nil { if err != nil {
*reply = "Plugin upload failed" *reply = "Plugin upload failed"
return err return err
@@ -506,8 +589,8 @@ func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error {
} }
for _, domain := range newDomains { for _, domain := range newDomains {
_, ok := rpc.gloom.hostMap.Load(domain) exists, _ := rpc.gloom.hostMap.Load(domain)
if ok { if exists {
*reply = fmt.Sprintf("Domain %s already exists", domain) *reply = fmt.Sprintf("Domain %s already exists", domain)
return nil return nil
} }
@@ -576,14 +659,11 @@ func (rpc *GloomRPC) DeletePlugin(pluginName string, reply *string) error {
return nil return nil
} }
_, err := rpc.gloom.DB.Exec("DELETE FROM plugins WHERE name = ?", pluginName) if err := rpc.gloom.DeletePlugin(pluginName); err != nil {
if err != nil { *reply = fmt.Sprintf("Failed to delete plugin: %v", err)
*reply = "Plugin not found"
return err return err
} }
rpc.gloom.DeletePlugin(pluginName)
*reply = "Plugin deleted successfully" *reply = "Plugin deleted successfully"
return nil return nil
} }

View File

@@ -23,4 +23,6 @@ To run the plugin host, run the following command:
- `pluginPath` - The path to the plugin to load. - `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. - `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. - `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. This connection is destroyed when a message
is sent to the socket.

View File

@@ -46,15 +46,35 @@ func main() {
signalChan := make(chan os.Signal, 1) signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt) signal.Notify(signalChan, os.Interrupt)
var listener net.Listener
var router *fiber.App
go func() { go func() {
<-signalChan <-signalChan
// TODO: maybe do something graceful here
fmt.Println("Received SIGINT, shutting down...") fmt.Println("Received SIGINT, shutting down...")
if router != nil {
if err := router.Shutdown(); err != nil {
log.Printf("Error shutting down router: %v", err)
}
}
if listener != nil {
if err := listener.Close(); err != nil {
log.Printf("Error closing listener: %v", err)
}
os.Remove(socketPath)
}
os.Exit(0) os.Exit(0)
}() }()
var writer io.Writer var Print = func(format string, args ...any) {
writer = os.Stderr fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintf(os.Stderr, "\n")
}
if controlPath != "" { if controlPath != "" {
fmt.Printf("Waiting for control connection on %s\n", controlPath) fmt.Printf("Waiting for control connection on %s\n", controlPath)
@@ -72,7 +92,15 @@ func main() {
defer conn.Close() defer conn.Close()
var ok bool var ok bool
var writer io.Writer
writer, ok = conn.(io.Writer) writer, ok = conn.(io.Writer)
// send the one message we can send and kill the connection
Print = func(format string, args ...any) {
fmt.Fprintf(writer, format, args...)
fmt.Fprintf(writer, "\n")
conn.Close()
os.Remove(controlPath)
}
if !ok { if !ok {
log.Printf("Control connection is not a writer") log.Printf("Control connection is not a writer")
return return
@@ -80,37 +108,37 @@ func main() {
} }
if _, err := os.Stat(socketPath); err == nil { if _, err := os.Stat(socketPath); err == nil {
fmt.Fprintf(writer, "Error: Socket %s already exists\n", socketPath) Print("Error: Socket %s already exists", socketPath)
os.Exit(1) os.Exit(1)
} }
realPluginPath, err := filepath.Abs(pluginPath) realPluginPath, err := filepath.Abs(pluginPath)
if err != nil { if err != nil {
fmt.Fprintf(writer, "Error: could not get absolute plugin path: %v\n", err) Print("Error: could not get absolute plugin path: %v", err)
os.Exit(1) os.Exit(1)
} }
p, err := plugin.Open(realPluginPath) p, err := plugin.Open(realPluginPath)
if err != nil { if err != nil {
fmt.Fprintf(writer, "Error: could not open plugin %s: %v\n", realPluginPath, err) Print("Error: could not open plugin %s: %v", realPluginPath, err)
os.Exit(1) os.Exit(1)
} }
symbol, err := p.Lookup("Plugin") symbol, err := p.Lookup("Plugin")
if err != nil { if err != nil {
fmt.Fprintf(writer, "Error: could not find 'Plugin' symbol in %s: %v\n", realPluginPath, err) Print("Error: could not find 'Plugin' symbol in %s: %v", realPluginPath, err)
os.Exit(1) os.Exit(1)
} }
pluginLib, ok := symbol.(Plugin) pluginLib, ok := symbol.(Plugin)
if !ok { if !ok {
fmt.Fprintf(writer, "Error: symbol 'Plugin' in %s is not a Plugin interface\n", realPluginPath) Print("Error: symbol 'Plugin' in %s is not a Plugin interface", realPluginPath)
os.Exit(1) os.Exit(1)
} }
pluginConfig, err := pluginLib.Init() pluginConfig, err := pluginLib.Init()
if err != nil { if err != nil {
fmt.Fprintf(writer, "Error: error initializing plugin %s: %v\n", realPluginPath, err) Print("Error: error initializing plugin %s: %v", realPluginPath, err)
os.Exit(1) os.Exit(1)
} }
@@ -118,21 +146,25 @@ func main() {
if pluginConfig != nil { if pluginConfig != nil {
config = *pluginConfig config = *pluginConfig
} }
router := fiber.New(config) router = fiber.New(config)
pluginLib.RegisterRoutes(router) pluginLib.RegisterRoutes(router)
// listen for connections on the socket // listen for connections on the socket
listener, err := net.Listen("unix", socketPath) listener, err = net.Listen("unix", socketPath)
if err != nil { if err != nil {
fmt.Fprintf(writer, "Error: error listening on socket %s: %v\n", socketPath, err) Print("Error: error listening on socket %s: %v", socketPath, err)
os.Exit(1) os.Exit(1)
} }
fmt.Fprintf(writer, "ready\n") if err := router.Listener(listener, fiber.ListenConfig{
// technically this can still error
router.Listener(listener, fiber.ListenConfig{
DisableStartupMessage: true, DisableStartupMessage: true,
}) BeforeServeFunc: func(app *fiber.App) error {
Print("ready")
return nil
},
}); err != nil {
Print("Error: error starting server: %v", err)
os.Exit(1)
}
} }

View File

@@ -6,11 +6,12 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "zqdgr build:gloomi && zqdgr build:gloom", "build": "zqdgr build:gloomi && zqdgr build:gloom",
"build:pluginHost": "sh -c \"cd pluginHost; go build -o ../host main.go\"", "build:pluginHost": "sh -c \"cd pluginHost; go build -ldflags '-w -s' -o ../dist/host main.go\"",
"build:gloomi": "sh -c \"cd gloomi; zqdgr build\"", "build:gloomi": "sh -c \"cd gloomi; zqdgr build\"",
"build:gloom": "zqdgr build:pluginHost && go build", "build:gloom": "zqdgr build:pluginHost && go build -tags=gloomi -o dist/gloom",
"clean": "rm -rf plugs && rm -rf host && rm -rf gloom.db && rm -rf plugin/plugin.so && rm -rf gloom", "build:nogloomi": "zqdgr build:pluginHost && go build -tags=!gloomi -o dist/gloom",
"dev": "zqdgr build && ./gloom" "clean": "rm -rf plugs && rm -rf dist && rm -rf plugin/plugin.so",
"dev": "zqdgr build && ./dist/gloom"
}, },
"pattern": "**/*.go", "pattern": "**/*.go",
"excluded_dirs": [] "excluded_dirs": []