diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index c213f1f..af7dd7d 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -55,6 +55,9 @@ type PluginUpload struct { } ``` +> [!IMPORTANT] +> The Name field can only contain alphanumeric characters and underscores and dashes. + ## 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: diff --git a/README.md b/README.md index 6ba5052..df0ba58 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,17 @@ GLoom is a plugin-based web app manager written in Go (perhaps a pico-paas). GLo zqdgr build:no-gloomi ``` - and make sure to set the `DISABLE_GLOOMI` environment variable to `true` in the `.env` file. + 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. ## 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. +- `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"]}] + ``` - `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 diff --git a/gloomi/main.go b/gloomi/main.go index 0d03ce8..2df4a01 100644 --- a/gloomi/main.go +++ b/gloomi/main.go @@ -52,6 +52,7 @@ func (p *GLoomI) RegisterRoutes(router fiber.Router) { }) type UploadRequest struct { + Name string `form:"name"` Domains string `form:"domains"` } @@ -65,6 +66,17 @@ func (p *GLoomI) RegisterRoutes(router fiber.Router) { return c.Status(fiber.StatusBadRequest).SendString("No domains provided") } + if pluginUpload.Name == "" { + return c.Status(fiber.StatusBadRequest).SendString("No name provided") + } + + // check if string is alphanumeric + for _, char := range pluginUpload.Name { + if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '-' || char == '_') { + return c.Status(fiber.StatusBadRequest).SendString("Invalid name provided") + } + } + domains := make([]string, 0) for _, domain := range strings.Split(pluginUpload.Domains, ",") { domains = append(domains, strings.TrimSpace(domain)) @@ -86,7 +98,7 @@ func (p *GLoomI) RegisterRoutes(router fiber.Router) { Name string `json:"name"` Data []byte `json:"data"` } - pluginUploadStruct.Name = pluginFile.Filename + pluginUploadStruct.Name = pluginUpload.Name pluginUploadStruct.Domains = domains pluginUploadStruct.Data, err = io.ReadAll(pluginData) if err != nil { diff --git a/main.go b/main.go index 19ca9c0..3128436 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "context" "database/sql" "embed" + "encoding/json" "fmt" "log/slog" "math/rand/v2" @@ -37,11 +38,18 @@ type PluginHost struct { Domains []string } +type PreloadPlugin struct { + File string `json:"file"` + Domains []string `json:"domains"` +} + type GLoom struct { // path to the pluginHost binary tmpDir string pluginDir string + preloadPlugins []PreloadPlugin + plugins libs.SyncMap[string, *PluginHost] hostMap libs.SyncMap[string, bool] @@ -95,12 +103,29 @@ func NewGloom(proxyManager *ProxyManager) (*GLoom, error) { } slog.Debug("Wrote pluginHost", "dir", tmpDir+"/pluginHost") + var preloadPlugins []PreloadPlugin + preloadPluginsEnv, ok := os.LookupEnv("PRELOAD_PLUGINS") + if ok { + err = json.Unmarshal([]byte(preloadPluginsEnv), &preloadPlugins) + if err != nil { + panic(err) + } + } else { + preloadPlugins = []PreloadPlugin{ + { + File: "gloomi.so", + Domains: []string{"localhost"}, + }, + } + } + gloom := &GLoom{ - tmpDir: tmpDir, - pluginDir: pluginsDir, - plugins: libs.SyncMap[string, *PluginHost]{}, - DB: db, - ProxyManager: proxyManager, + tmpDir: tmpDir, + pluginDir: pluginsDir, + preloadPlugins: preloadPlugins, + plugins: libs.SyncMap[string, *PluginHost]{}, + DB: db, + ProxyManager: proxyManager, } return gloom, nil @@ -109,6 +134,12 @@ func NewGloom(proxyManager *ProxyManager) (*GLoom, error) { func (gloom *GLoom) LoadInitialPlugins() error { slog.Debug("Loading initial plugins") + for _, plugin := range gloom.preloadPlugins { + if err := gloom.RegisterPlugin(filepath.Join(gloom.pluginDir, plugin.File), plugin.File, plugin.Domains); err != nil { + slog.Warn("Failed to register plugin", "pluginPath", plugin.File, "error", err) + } + } + plugins, err := gloom.DB.Query("SELECT path, domains, name FROM plugins") if err != nil { return err @@ -272,7 +303,7 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []str Domains: domains, } - gloom.plugins.Store(pluginPath, plugHost) + gloom.plugins.Store(name, plugHost) if oldProxy != nil { go func() { @@ -367,6 +398,32 @@ type PluginUpload struct { } func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error { + for _, preloadPlugin := range rpc.gloom.preloadPlugins { + if plugin.Name == preloadPlugin.File { + *reply = "Plugin is preloaded" + return nil + } + } + + if plugin.Name == "" { + *reply = "Plugin name cannot be empty" + return fmt.Errorf("plugin name cannot be empty") + } + + for _, char := range plugin.Name { + if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '-' || char == '_') { + *reply = "Invalid plugin name" + return fmt.Errorf("invalid plugin name") + } + } + + for _, domain := range plugin.Domains { + if domain == "" { + *reply = "Domain cannot be empty" + return fmt.Errorf("domain cannot be empty") + } + } + _, err := deploymentLock.Lock(plugin.Name, context.Background()) if err != nil && err == ErrLocked { *reply = "Plugin is already being updated" @@ -492,9 +549,11 @@ func (rpc *GloomRPC) UploadPlugin(plugin PluginUpload, reply *string) error { } func (rpc *GloomRPC) DeletePlugin(pluginName string, reply *string) error { - if pluginName == "GLoomI" { - *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" - return nil + for _, preloadPlugin := range rpc.gloom.preloadPlugins { + if pluginName == preloadPlugin.File { + *reply = "Plugin is preloaded" + return nil + } } _, ok := rpc.gloom.plugins.Load(pluginName) @@ -548,22 +607,6 @@ func main() { gloom.LoadInitialPlugins() - enableGloomi, err := strconv.ParseBool(os.Getenv("ENABLE_GLOOMI")) - if err != nil { - enableGloomi = true - } - - if enableGloomi { - hostname := os.Getenv("GLOOMI_HOSTNAME") - if hostname == "" { - hostname = "127.0.0.1" - } - - if err := gloom.RegisterPlugin("plugs/gloomi.so", "GLoomI", []string{hostname}); err != nil { - panic("Failed to register GLoomI: " + err.Error()) - } - } - fmt.Println("Server running at http://localhost:3000") if err := gloom.ProxyManager.ListenAndServe("127.0.0.1:3000"); err != nil { panic(err)