diff --git a/pkg/service/profile.go b/pkg/service/profile.go index 6bcdebe..2fa7c2d 100644 --- a/pkg/service/profile.go +++ b/pkg/service/profile.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" + configurator "git.towk2.me/towk/configurator/pkg" "github.com/go-chi/chi/v5" "github.com/tidwall/sjson" ) @@ -86,22 +87,14 @@ func (s *Service) GetProfile() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var ( id = chi.URLParam(r, "id") - path = s.BuildProfilePath(id) - profile *Profile + path = s.PathForProfileWithID(id) contents []byte err error ) - profile, err = LoadProfileFromFile(path) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - contents, err = json.Marshal(profile) + contents, err = loadProfileContents(path) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) - return } _, err = w.Write(contents) @@ -184,7 +177,7 @@ func (s *Service) SetProfileData() http.HandlerFunc { } // read the contents the file with profile ID - path = s.BuildProfilePath(profile.ID) + path = s.PathForProfileWithID(profile.ID) contents, err = os.ReadFile(path) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -209,28 +202,104 @@ func (s *Service) SetProfileData() http.HandlerFunc { func (s *Service) DeleteProfileData() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + var ( + id = chi.URLParam(r, "id") + path = s.PathForProfileWithID(id) + profile *Profile + err error + ) + + // get the profile + profile, err = LoadProfileFromFile(path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + + // delete the profile data + profile.Data = map[string]any{} + + // save the profile back to the file to update + SaveProfileToFile(path, profile) } } func (s *Service) GetProfileData() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) {} + return func(w http.ResponseWriter, r *http.Request) { + var ( + id = chi.URLParam(r, "id") + path = s.PathForProfileWithID(id) + profile *Profile + body []byte + err error + ) + + profile, err = LoadProfileFromFile(path) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // only marshal the profile data and not entire profile + body, err = json.Marshal(profile.Data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // write body to response + _, err = w.Write(body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } } -func (s *Service) CreateProfilePath() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) {} -} +// func (s *Service) CreateProfilePath() http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) { -func (s *Service) DeleteProfilePath() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) {} -} +// w.WriteHeader(http.StatusOK) +// } +// } -func (s *Service) GetProfilePath() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) {} -} +// func (s *Service) DeleteProfilePath() http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) {} +// } + +// func (s *Service) GetProfilePath() http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) {} +// } func (s *Service) GetPlugins() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) {} + + return func(w http.ResponseWriter, r *http.Request) { + var ( + plugins map[string]configurator.Plugin + names []string + body []byte + err error + ) + + plugins, err = LoadPluginsFromDir(s.PathForPlugins()) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + for name := range plugins { + names = append(names, name) + } + + body, err = json.Marshal(names) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Write(body) + } } func (s *Service) CreatePlugins() http.HandlerFunc { @@ -240,3 +309,21 @@ func (s *Service) CreatePlugins() http.HandlerFunc { func (s *Service) DeletePlugins() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) {} } + +func loadProfileContents(path string) ([]byte, error) { + var ( + contents []byte + profile *Profile + err error + ) + profile, err = LoadProfileFromFile(path) + if err != nil { + return nil, fmt.Errorf("failed to load profile from file: %v", err) + } + + contents, err = json.Marshal(profile) + if err != nil { + return nil, fmt.Errorf("failed to marshal profile: %v", err) + } + return contents, nil +} diff --git a/pkg/service/service.go b/pkg/service/service.go index 50483af..c208b15 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -3,11 +3,16 @@ 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" ) @@ -66,9 +71,9 @@ func (s *Service) Serve() error { 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()) + // 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()) @@ -90,14 +95,99 @@ func (s *Service) FetchJwks(uri string) { } func LoadProfileFromFile(path string) (*Profile, error) { - return LoadFromJSONFile[Profile](path) + return loadFromJSONFile[Profile](path) } -func LoadPluginFromFile(path string) (*configurator.Plugin, error) { - return LoadFromJSONFile[configurator.Plugin](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 } -func LoadFromJSONFile[T any](path string) (*T, error) { +// 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 @@ -117,6 +207,27 @@ func LoadFromJSONFile[T any](path string) (*T, error) { return res, err } -func (s *Service) BuildProfilePath(id string) string { - return s.RootPath + PLUGINS_RELPATH + "/" + id +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 }