diff --git a/gloomi/main.go b/gloomi/main.go index 1a65e26..3ac3f4c 100644 --- a/gloomi/main.go +++ b/gloomi/main.go @@ -6,7 +6,6 @@ import ( "mime/multipart" "net/rpc" "strings" - "time" "github.com/gofiber/fiber/v3" ) @@ -133,4 +132,3 @@ func (p *GLoomI) RegisterRoutes(router fiber.Router) { // Exported symbol var Plugin GLoomI -var Version = time.Now() diff --git a/main.go b/main.go index 5ef66c9..9b56cf6 100644 --- a/main.go +++ b/main.go @@ -67,6 +67,48 @@ func hasChangeUserAbility() bool { return permitted } +func IsExecutableNewer(filePath string) bool { + // Get the location of the gloom binary + gloomPath, err := os.Executable() + if err != nil { + return false + } + + gloomPath, err = filepath.EvalSymlinks(gloomPath) + if err != nil { + return false + } + + fileInfo, err := os.Stat(gloomPath) + if err != nil { + return false + } + + childInfo, err := os.Stat(filePath) + if err != nil { + return false + } + + return fileInfo.ModTime().After(childInfo.ModTime()) +} + +func hasChangeResourceLimitAbility() bool { + cap, err := capability.NewPid2(0) + if err != nil { + return false + } + + err = cap.Load() + if err != nil { + return false + } + + return cap.Get(capability.EFFECTIVE, capability.CAP_SYS_RESOURCE) || cap.Get(capability.PERMITTED, capability.CAP_SYS_RESOURCE) +} + +// this is the default for user 1000 on my system +const NOFILE_LIMIT = 1048576 + type PluginHost struct { UnixSocket string Process *os.Process @@ -151,12 +193,24 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) { return nil, err } - if _, err := os.Stat(filepath.Join(gloomDir, "pluginHost")); os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(gloomDir, "pluginHost")); err != nil { if err := os.WriteFile(filepath.Join(gloomDir, "pluginHost"), pluginHost, 0755); err != nil { return nil, err } slog.Debug("Wrote pluginHost", "dir", filepath.Join(gloomDir, "pluginHost")) + } else { + if IsExecutableNewer(filepath.Join(gloomDir, "pluginHost")) { + slog.Debug("Replacing pluginHost", "dir", filepath.Join(gloomDir, "pluginHost"), "reason", "host is old") + + if err := os.WriteFile(filepath.Join(gloomDir, "pluginHost"), pluginHost, 0755); err != nil { + return nil, err + } + } + } + + if err := os.Chmod(filepath.Join(gloomDir, "pluginHost"), 0777); err != nil { + return nil, err } gloom := &GLoom{ @@ -180,6 +234,10 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) { if !hasChangeUserAbility() { return nil, fmt.Errorf("chroot is enabled, but you do not have the required privileges to use it") } + + if !hasChangeResourceLimitAbility() { + return nil, fmt.Errorf("chroot is enabled, but you do not have the required privileges to use it") + } } if err := os.MkdirAll(gloom.config.PluginDir, 0755); err != nil { @@ -189,21 +247,33 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) { // 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 - // TODO: instead, check if the plugin doesnt exist OR the binary has a newer timestamp than the current version + // ensure the plugin directory exists if err := os.MkdirAll(filepath.Join(gloom.config.PluginDir, "GLoomI"), 0755); err != nil { return nil, err } - if _, err := os.Stat(filepath.Join(gloom.config.PluginDir, "GLoomI", "gloomi.so")); os.IsNotExist(err) { - gloomiData, err := embeddedAssets.ReadFile("dist/gloomi.so") - if err != nil { - return nil, err - } + gloomiData, err := embeddedAssets.ReadFile("dist/gloomi.so") + if err != nil { + return nil, err + } + + gloomiPath := filepath.Join(gloom.config.PluginDir, "GLoomI", "gloomi.so") + + if _, err := os.Stat(gloomiPath); err != nil { + // if the plugin doesn't exist, copy it over + slog.Debug("Replacing gloomi", "pluginPath", gloomiPath, "reason", "plugin doesnt exist") if err := os.WriteFile(filepath.Join(gloom.config.PluginDir, "GLoomI", "gloomi.so"), gloomiData, 0755); err != nil { return nil, err } + } else { + if IsExecutableNewer(gloomiPath) { + slog.Debug("Replacing gloomi", "pluginPath", gloomiPath, "reason", "plugin is old") + + if err := os.WriteFile(gloomiPath, gloomiData, 0755); err != nil { + return nil, err + } + } } } @@ -249,16 +319,16 @@ func (gloom *GLoom) LoadInitialPlugins() error { slog.Info("Loading initial plugins") for _, plugin := range gloom.config.PreloadPlugins { - // if plugin is in PluginDir, we should move it to its named folder + // if a plugin is marked as preload, but its not organized in the way that GLoom would set it up, we should move + // the plugin to its named folder if _, err := os.Stat(filepath.Join(gloom.config.PluginDir, plugin.File)); err == nil { slog.Debug("Moving plugin to its named folder", "plugin", plugin.File) - if err := os.MkdirAll(filepath.Join(gloom.config.PluginDir, plugin.Name), 0755); err != nil { - panic(fmt.Errorf("failed to create plugin folder: %w", err)) + return fmt.Errorf("failed to create plugin folder: %w", err) } if err := os.Rename(filepath.Join(gloom.config.PluginDir, plugin.File), filepath.Join(gloom.config.PluginDir, plugin.Name, plugin.File)); err != nil { - panic(fmt.Errorf("failed to move plugin to its named folder: %w", err)) + return fmt.Errorf("failed to move plugin to its named folder: %w", err) } } @@ -266,7 +336,7 @@ func (gloom *GLoom) LoadInitialPlugins() error { path := filepath.Join(gloom.config.PluginDir, plugin.Name, plugin.File) if err := gloom.RegisterPlugin(path, plugin.Name, plugin.Domains); err != nil { - panic(fmt.Errorf("failed to load preload plugin %s: %w (make sure its in %s)", plugin.Name, err, path)) + return fmt.Errorf("failed to load preload plugin %s: %w (make sure its in %s)", plugin.Name, err, path) } } @@ -436,7 +506,7 @@ func (gloom *GLoom) RegisterPlugin(pluginPath string, name string, domains []str status = strings.TrimSpace(status) if status == "ready" { - slog.Debug("PluginHost ported ready", "pluginPath", pluginPath) + slog.Debug("PluginHost ready", "pluginPath", pluginPath, "hostPid", process.Pid) break } else if strings.HasPrefix(status, "Error: ") { errorMessage := strings.TrimPrefix(status, "Error: ") @@ -773,10 +843,8 @@ func (rpc *GloomRPC) DeletePlugin(pluginName string, reply *string) error { } func main() { - debug, err := strconv.ParseBool(os.Getenv("DEBUG")) - if err != nil { - debug = false - } + // no need to check error value, it will always be a valid bool + debug, _ := strconv.ParseBool(os.Getenv("DEBUG")) level := slog.LevelInfo if debug { @@ -796,14 +864,20 @@ func main() { defer gloom.Cleanup() if err := gloom.StartRPCServer(); err != nil { - panic("Failed to start RPC server: " + err.Error()) + slog.Error("Failed to start RPC server", "error", err) + os.Exit(1) } - gloom.LoadInitialPlugins() + err = gloom.LoadInitialPlugins() + if err != nil { + slog.Error("Failed to load initial plugins", "error", err) + os.Exit(1) + } slog.Info("Server running at http://localhost:3000") if err := gloom.ProxyManager.ListenAndServe("127.0.0.1:3000"); err != nil { - panic(err) + slog.Error("Failed to start proxy server", "error", err) + os.Exit(1) } } diff --git a/plugin/main.go b/plugin/main.go index 820af88..1f0f5c9 100644 --- a/plugin/main.go +++ b/plugin/main.go @@ -1,6 +1,12 @@ package main -import "github.com/gofiber/fiber/v3" +import ( + "fmt" + "os" + "path/filepath" + + "github.com/gofiber/fiber/v3" +) type MyPlugin struct{} @@ -16,6 +22,60 @@ func (p *MyPlugin) RegisterRoutes(router fiber.Router) { router.Get("/hello", func(c fiber.Ctx) error { return c.SendString("Hello from MyPlugin!") }) + + router.Get("/dir/:path?", func(c fiber.Ctx) error { + // Get the directory path from the URL parameter + // If the parameter is empty, default to the current working directory. + dirPath := c.Params("path") + if dirPath == "" { + var err error + dirPath, err = os.Getwd() // Get current working directory + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Failed to get current working directory", + }) + } + } + + // Ensure the path is absolute for security and clarity + absPath, err := filepath.Abs(dirPath) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Failed to get absolute path for %s: %v", dirPath, err), + }) + } + + // Read the directory contents + files, err := os.ReadDir(absPath) + if err != nil { + // Handle different error types for better feedback + if os.IsNotExist(err) { + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ + "error": fmt.Sprintf("Directory not found: %s", absPath), + }) + } + if os.IsPermission(err) { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": fmt.Sprintf("Permission denied to access directory: %s", absPath), + }) + } + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": fmt.Sprintf("Failed to read directory %s: %v", absPath, err), + }) + } + + // Extract file names and prepare the response + var fileNames []string + for _, file := range files { + fileNames = append(fileNames, file.Name()) + } + + // Return the list of file names as JSON + return c.JSON(fiber.Map{ + "directory": absPath, + "files": fileNames, + }) + }) } // Exported symbol diff --git a/pluginHost/main.go b/pluginHost/main.go index 98e10ca..fc656e1 100644 --- a/pluginHost/main.go +++ b/pluginHost/main.go @@ -8,8 +8,10 @@ import ( "net" "os" "os/signal" + "os/user" "path/filepath" "plugin" + "strconv" "strings" "syscall" @@ -147,51 +149,63 @@ func main() { } // we are chrooting and changing to nobody to "sandbox" the plugin - // nobodyUser, err := user.Lookup("nobody") - // if err != nil { - // Print("Error: failed to get nobody user: %w", err) - // } - - // nobodyUid, err := strconv.Atoi(nobodyUser.Uid) - // if err != nil { - // Print("Error: failed to parse nobody uid: %w", err) - // } - - // nobodyGid, err := strconv.Atoi(nobodyUser.Gid) - // if err != nil { - // Print("Error: failed to parse nobody gid: %w", err) - // } - - pluginData, err := os.ReadFile(realPluginPath) + nobodyUser, err := user.Lookup("nobody") if err != nil { - Print("Error: failed to read plugin: %w", err) + Print("Error: failed to get nobody user: %w", err) } - pluginFileName := filepath.Base(realPluginPath) - - // copy the plugin to the chroot directory - if err := os.WriteFile(filepath.Join(chrootDir, pluginFileName), pluginData, 0644); err != nil { - Print("Error: failed to copy plugin to chroot: %w", err) + nobodyUid, err := strconv.Atoi(nobodyUser.Uid) + if err != nil { + Print("Error: failed to parse nobody uid: %w", err) } - // if err := os.Chown(chrootDir, nobodyUid, nobodyGid); err != nil { - // Print("Error: failed to chown chroot directory: %w", err) - // } + nobodyGid, err := strconv.Atoi(nobodyUser.Gid) + if err != nil { + Print("Error: failed to parse nobody gid: %w", err) + } - realPluginPath = "/" + pluginFileName + if err := os.Chown(chrootDir, nobodyUid, nobodyGid); err != nil { + Print("Error: failed to chown chroot directory: %w", err) + } + + if err := os.Chown(realPluginPath, nobodyUid, nobodyGid); err != nil { + Print("Error: failed to chown plugin directory: %w", err) + } + + chrootTmp := filepath.Join(chrootDir, "tmp") + + if err := os.RemoveAll(chrootTmp); err != nil { + Print("Error: failed to remove chroot tmp directory: %w", err) + } + + if err := os.MkdirAll(chrootTmp, 0755); err != nil { + Print("Error: failed to create chroot tmp directory: %w", err) + } + + // we could bind os.TempDir() to chrootTmp, but that would allow for plugins to read eachothers temp files, so + // instead we'll create a new temp directory specifically for the plugin that gets clearned on exit and startup. + defer func() { + os.RemoveAll(chrootTmp) + }() + + realPluginPath = "/" + filepath.Base(realPluginPath) socketPath = "/" + filepath.Base(socketPath) if err := syscall.Chroot(chrootDir); err != nil { Print("Error: failed to chroot: %w", err) } - // if err := syscall.Setgid(nobodyGid); err != nil { - // Print("Error: failed to setgid: %w", err) - // } + if err := syscall.Chdir("/"); err != nil { + Print("Error: failed to chdir: %w", err) + } - // if err := syscall.Setuid(nobodyUid); err != nil { - // Print("Error: failed to setuid: %w", err) - // } + if err := syscall.Setgid(nobodyGid); err != nil { + Print("Error: failed to setgid: %w", err) + } + + if err := syscall.Setuid(nobodyUid); err != nil { + Print("Error: failed to setuid: %w", err) + } } p, err := plugin.Open(realPluginPath) @@ -233,6 +247,11 @@ func main() { os.Exit(1) } + if err := os.Chmod(socketPath, 0666); err != nil { + Print("Error: failed to chmod socket: %v", err) + os.Exit(1) + } + if err := router.Listener(listener, fiber.ListenConfig{ DisableStartupMessage: true, BeforeServeFunc: func(app *fiber.App) error {