package service import ( "encoding/json" "fmt" "io/fs" "net/http" "os" "path/filepath" "plugin" "slices" "time" configurator "git.towk2.me/towk/configurator/pkg" "git.towk2.me/towk/configurator/pkg/util" "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" ) const ( PLUGINS_RELPATH = "/plugins" TEMPLATES_RELPATH = "/templates" PROFILES_RELPATH = "/profiles" ) type Service struct { RootPath string `yaml:"root,omitempty"` Environment map[string]string // max counts PluginsMaxCount int ProfilesMaxCount int } // New creates the directories at specified path func New() *Service { return &Service{ RootPath: ".", Environment: map[string]string{ "CONFIGURATOR_HOST_URI": "", "ACCESS_TOKEN": "", }, PluginsMaxCount: 64, ProfilesMaxCount: 256, } } // Serve() starts the configurator service and waits for requests. func (s *Service) Serve() error { router := chi.NewRouter() router.Use(middleware.RequestID) router.Use(middleware.RealIP) router.Use(middleware.Logger) router.Use(middleware.Recoverer) router.Use(middleware.StripSlashes) router.Use(middleware.Timeout(60 * time.Second)) if s.requireAuth() { } else { // general router.Get("/download/*", s.Download()) router.Post("/upload", s.Upload()) router.Get("/list", s.List()) // profiles router.Get("/profiles", s.GetProfiles()) // router.Post("/profiles", s.CreateProfiles()) router.Get("/profile/{id}", s.GetProfile()) router.Post("/profile/{id}", s.CreateProfile()) router.Get("/profile/{id}/data", s.GetProfileData()) router.Post("/profile/{id}/data", s.SetProfileData()) router.Delete("/profile/{id}/data", s.DeleteProfileData()) // router.Post("/profile/{id}/paths/{path}", s.CreateProfilePath()) // router.Delete("/profile/{id}/paths/{path}", s.DeleteProfilePath()) // router.Get("/profile/{id}/paths/{path}", s.GetProfilePath()) // plugins router.Get("/plugins", s.GetPlugins()) router.Post("/plugins", s.CreatePlugins()) router.Delete("/plugins/{id}", s.DeletePlugins()) } // always available public routes go here router.HandleFunc("/status", s.GetStatus) return http.ListenAndServe(":8080", router) } func (s *Service) requireAuth() bool { return false } func (s *Service) FetchJwks(uri string) { } func LoadProfileFromFile(path string) (*Profile, error) { return loadFromJSONFile[Profile](path) } // LoadPluginFromFile loads a single plugin given a single file path func LoadPluginFromFile(path string) (configurator.Plugin, error) { var ( isDir bool err error loadedPlugin *plugin.Plugin ) // skip loading plugin if path is a directory with no error if isDir, err = util.IsDirectory(path); err == nil && isDir { return nil, nil } else if err != nil { return nil, fmt.Errorf("failed to test if plugin path is directory: %v", err) } // try and open the plugin loadedPlugin, err = plugin.Open(path) if err != nil { return nil, fmt.Errorf("failed to open plugin at path '%s': %v", path, err) } // load the "Target" symbol from plugin symbol, err := loadedPlugin.Lookup("Target") if err != nil { return nil, fmt.Errorf("failed to look up symbol at path '%s': %v", path, err) } // assert that the plugin is a valid configurator.Plugin target, ok := symbol.(configurator.Plugin) if !ok { return nil, fmt.Errorf("failed to load the correct symbol type at path '%s'", path) } return target, nil } // LoadPluginsFromDir loads all plugins in a given directory. // // Returns a map of plugins. Each plugin can be accessed by the name // returned by the plugin.GetName() implemented. func LoadPluginsFromDir(dirpath string) (map[string]configurator.Plugin, error) { // check if verbose option is supplied var ( cps = make(map[string]configurator.Plugin) err error ) // helper to check for valid extensions var hasValidExt = func(path string) bool { var validExts = []string{".so", ".dylib", ".dll"} return slices.Contains(validExts, filepath.Ext(path)) } // walk all files in directory only loading *valid* plugins err = filepath.Walk(dirpath, func(path string, info fs.FileInfo, err error) error { // skip trying to load generator plugin if directory or error if info.IsDir() || err != nil { return nil } // only try loading if file has .so extension if hasValidExt(path) { return nil } // load the plugin from current path p, err := LoadPluginFromFile(path) if err != nil { return fmt.Errorf("failed to load plugin in directory '%s': %v", path, err) } // map each plugin by name for lookup cps[p.Name()] = p return nil }) if err != nil { return nil, fmt.Errorf("failed to walk directory '%s': %v", dirpath, err) } return cps, nil } func SaveProfileToFile(path string, profile *Profile) error { return saveToJSONFile(path, profile) } func SavePluginToFile(path string, plugin *configurator.Plugin) error { return saveToJSONFile(path, plugin) } func loadFromJSONFile[T any](path string) (*T, error) { var ( res *T contents []byte err error ) contents, err = os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read plugin file: %v", err) } err = json.Unmarshal(contents, &res) if err != nil { return nil, fmt.Errorf("failed to unmarshal plugin: %v", err) } return res, err } func saveToJSONFile[T any](path string, data T) error { var ( contents []byte err error ) contents, err = json.Marshal(data) if err != nil { return fmt.Errorf("failed to marshal data to JSON: %v", err) } err = os.WriteFile(path, contents, os.ModePerm) if err != nil { return fmt.Errorf("failed to write JSON to file: %v", err) } return nil } func (s *Service) PathForProfileWithID(id string) string { return s.RootPath + PROFILES_RELPATH + "/" + id } func (s *Service) PathForPlugins() string { return s.RootPath + PLUGINS_RELPATH }