Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
fa319076d1
|
@@ -6,7 +6,6 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/rpc"
|
"net/rpc"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
@@ -133,4 +132,3 @@ func (p *GLoomI) RegisterRoutes(router fiber.Router) {
|
|||||||
|
|
||||||
// Exported symbol
|
// Exported symbol
|
||||||
var Plugin GLoomI
|
var Plugin GLoomI
|
||||||
var Version = time.Now()
|
|
||||||
|
|||||||
108
main.go
108
main.go
@@ -67,6 +67,48 @@ func hasChangeUserAbility() bool {
|
|||||||
return permitted
|
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 {
|
type PluginHost struct {
|
||||||
UnixSocket string
|
UnixSocket string
|
||||||
Process *os.Process
|
Process *os.Process
|
||||||
@@ -151,12 +193,24 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
|
|||||||
return nil, err
|
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 {
|
if err := os.WriteFile(filepath.Join(gloomDir, "pluginHost"), pluginHost, 0755); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("Wrote pluginHost", "dir", filepath.Join(gloomDir, "pluginHost"))
|
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{
|
gloom := &GLoom{
|
||||||
@@ -180,6 +234,10 @@ func NewGloom(proxyManager *sentinel.ProxyManager) (*GLoom, error) {
|
|||||||
if !hasChangeUserAbility() {
|
if !hasChangeUserAbility() {
|
||||||
return nil, fmt.Errorf("chroot is enabled, but you do not have the required privileges to use it")
|
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 {
|
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 gloomi is built into the binary
|
||||||
if _, err := embeddedAssets.Open("dist/gloomi.so"); err == nil {
|
if _, err := embeddedAssets.Open("dist/gloomi.so"); err == nil {
|
||||||
// and if the plugin doesn't exist, copy it over
|
// ensure the plugin directory exists
|
||||||
// TODO: instead, check if the plugin doesnt exist OR the binary has a newer timestamp than the current version
|
|
||||||
if err := os.MkdirAll(filepath.Join(gloom.config.PluginDir, "GLoomI"), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Join(gloom.config.PluginDir, "GLoomI"), 0755); err != nil {
|
||||||
return nil, err
|
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")
|
gloomiData, err := embeddedAssets.ReadFile("dist/gloomi.so")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err := os.WriteFile(filepath.Join(gloom.config.PluginDir, "GLoomI", "gloomi.so"), gloomiData, 0755); err != nil {
|
||||||
return nil, err
|
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")
|
slog.Info("Loading initial plugins")
|
||||||
|
|
||||||
for _, plugin := range gloom.config.PreloadPlugins {
|
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 {
|
if _, err := os.Stat(filepath.Join(gloom.config.PluginDir, plugin.File)); err == nil {
|
||||||
slog.Debug("Moving plugin to its named folder", "plugin", plugin.File)
|
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 {
|
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 {
|
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)
|
path := filepath.Join(gloom.config.PluginDir, plugin.Name, plugin.File)
|
||||||
if err := gloom.RegisterPlugin(path, plugin.Name, plugin.Domains); err != nil {
|
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)
|
status = strings.TrimSpace(status)
|
||||||
|
|
||||||
if status == "ready" {
|
if status == "ready" {
|
||||||
slog.Debug("PluginHost ported ready", "pluginPath", pluginPath)
|
slog.Debug("PluginHost ready", "pluginPath", pluginPath, "hostPid", process.Pid)
|
||||||
break
|
break
|
||||||
} else if strings.HasPrefix(status, "Error: ") {
|
} else if strings.HasPrefix(status, "Error: ") {
|
||||||
errorMessage := strings.TrimPrefix(status, "Error: ")
|
errorMessage := strings.TrimPrefix(status, "Error: ")
|
||||||
@@ -773,10 +843,8 @@ func (rpc *GloomRPC) DeletePlugin(pluginName string, reply *string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
debug, err := strconv.ParseBool(os.Getenv("DEBUG"))
|
// no need to check error value, it will always be a valid bool
|
||||||
if err != nil {
|
debug, _ := strconv.ParseBool(os.Getenv("DEBUG"))
|
||||||
debug = false
|
|
||||||
}
|
|
||||||
|
|
||||||
level := slog.LevelInfo
|
level := slog.LevelInfo
|
||||||
if debug {
|
if debug {
|
||||||
@@ -796,14 +864,20 @@ func main() {
|
|||||||
defer gloom.Cleanup()
|
defer gloom.Cleanup()
|
||||||
|
|
||||||
if err := gloom.StartRPCServer(); err != nil {
|
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")
|
slog.Info("Server running at http://localhost:3000")
|
||||||
if err := gloom.ProxyManager.ListenAndServe("127.0.0.1:3000"); err != nil {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/gofiber/fiber/v3"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
type MyPlugin struct{}
|
type MyPlugin struct{}
|
||||||
|
|
||||||
@@ -16,6 +22,60 @@ func (p *MyPlugin) RegisterRoutes(router fiber.Router) {
|
|||||||
router.Get("/hello", func(c fiber.Ctx) error {
|
router.Get("/hello", func(c fiber.Ctx) error {
|
||||||
return c.SendString("Hello from MyPlugin!")
|
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
|
// Exported symbol
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"plugin"
|
"plugin"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
@@ -147,51 +149,63 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we are chrooting and changing to nobody to "sandbox" the plugin
|
// we are chrooting and changing to nobody to "sandbox" the plugin
|
||||||
// nobodyUser, err := user.Lookup("nobody")
|
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Print("Error: failed to read plugin: %w", err)
|
Print("Error: failed to get nobody user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginFileName := filepath.Base(realPluginPath)
|
nobodyUid, err := strconv.Atoi(nobodyUser.Uid)
|
||||||
|
if err != nil {
|
||||||
// copy the plugin to the chroot directory
|
Print("Error: failed to parse nobody uid: %w", err)
|
||||||
if err := os.WriteFile(filepath.Join(chrootDir, pluginFileName), pluginData, 0644); err != nil {
|
|
||||||
Print("Error: failed to copy plugin to chroot: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if err := os.Chown(chrootDir, nobodyUid, nobodyGid); err != nil {
|
nobodyGid, err := strconv.Atoi(nobodyUser.Gid)
|
||||||
// Print("Error: failed to chown chroot directory: %w", err)
|
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)
|
socketPath = "/" + filepath.Base(socketPath)
|
||||||
|
|
||||||
if err := syscall.Chroot(chrootDir); err != nil {
|
if err := syscall.Chroot(chrootDir); err != nil {
|
||||||
Print("Error: failed to chroot: %w", err)
|
Print("Error: failed to chroot: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if err := syscall.Setgid(nobodyGid); err != nil {
|
if err := syscall.Chdir("/"); err != nil {
|
||||||
// Print("Error: failed to setgid: %w", err)
|
Print("Error: failed to chdir: %w", err)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if err := syscall.Setuid(nobodyUid); err != nil {
|
if err := syscall.Setgid(nobodyGid); err != nil {
|
||||||
// Print("Error: failed to setuid: %w", err)
|
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)
|
p, err := plugin.Open(realPluginPath)
|
||||||
@@ -233,6 +247,11 @@ func main() {
|
|||||||
os.Exit(1)
|
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{
|
if err := router.Listener(listener, fiber.ListenConfig{
|
||||||
DisableStartupMessage: true,
|
DisableStartupMessage: true,
|
||||||
BeforeServeFunc: func(app *fiber.App) error {
|
BeforeServeFunc: func(app *fiber.App) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user