package service import ( "encoding/json" "fmt" "io/fs" "net/http" "os" "path/filepath" "plugin" "slices" "time" makeshift "git.towk2.me/towk/makeshift/pkg" "git.towk2.me/towk/makeshift/pkg/util" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/rs/zerolog/log" ) type Service struct { Addr string RootPath string `yaml:"root,omitempty"` CACertFile string `yaml:"cacert,omitempty"` CACertKeyfile string `yaml:"keyfile,omitempty"` // max counts PluginsMaxCount int ProfilesMaxCount int Timeout time.Duration } // New creates a new Service instance with default values func New() *Service { return &Service{ Addr: ":5050", RootPath: "./", PluginsMaxCount: DEFAULT_PLUGINS_MAX_COUNT, ProfilesMaxCount: DEFAULT_PROFILES_MAX_COUNT, Timeout: DEFAULT_TIMEOUT_IN_SECS, } } // Init() sets up the default files and directories for the service func (s *Service) Init() error { // create the default directories var err error err = os.MkdirAll(s.RootPath, 0o777) if err != nil { return fmt.Errorf("failed to make service root path: %v", err) } err = os.MkdirAll(s.PathForPlugins(), 0o777) if err != nil { return fmt.Errorf("failed to make service plugin path: %v", err) } err = os.MkdirAll(s.PathForProfiles(), 0o777) if err != nil { return fmt.Errorf("failed to make service profile path: %v", err) } err = os.MkdirAll(s.PathForData()+"/www", 0o777) if err != nil { return fmt.Errorf("failed to make service data path: %v", err) } // create the default files err = os.WriteFile(s.PathForMetadata(), []byte(FILE_METADATA), 0o777) if err != nil { return fmt.Errorf("failed to make service metadata file: %v", err) } err = os.WriteFile(s.PathForHome(), []byte(FILE_HOME_PAGE), 0o777) if err != nil { return fmt.Errorf("failed to make service home page file: %v", err) } err = os.WriteFile(s.PathForProfileWithID("default"), []byte(FILE_DEFAULT_PROFILE), 0o777) if err != nil { return fmt.Errorf("failed to make service default profile file: %v", err) } return nil } // Serve() starts the makeshift 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(s.Timeout * time.Second)) if s.requireAuth() { } else { // general router.Get("/download/*", s.Download()) router.Post("/upload/*", s.Upload()) router.Get("/list/*", s.List()) router.Delete("/delete/*", s.Delete()) // profiles router.Get("/profiles", s.ListProfiles()) router.Get("/profiles/{id}", s.GetProfile()) router.Post("/profiles/{id}", s.CreateProfile()) router.Delete("/profiles/{id}", s.DeleteProfile()) router.Get("/profiles/{id}/data", s.GetProfileData()) router.Post("/profiles/{id}/data", s.SetProfileData()) router.Delete("/profiles/{id}/data", s.DeleteProfileData()) // plugins router.Get("/plugins", s.ListPlugins()) router.Get("/plugins/{name}/info", s.GetPluginInfo()) router.Get("/plugins/{name}/raw", s.GetPluginRaw()) router.Post("/plugins/{name}", s.CreatePlugin()) router.Delete("/plugins/{name}", s.DeletePlugin()) } // always available public routes go here router.HandleFunc("/status", s.GetStatus) if s.CACertFile != "" && s.CACertKeyfile != "" { return http.ListenAndServeTLS(s.Addr, s.CACertFile, s.CACertKeyfile, router) } else { return http.ListenAndServe(s.Addr, router) } } func (s *Service) requireAuth() bool { return false } func (s *Service) FetchJwks(uri string) { } func LoadProfileFromFile(path string) (*makeshift.Profile, error) { return loadFromJSONFile[makeshift.Profile](path) } func LoadProfilesFromDir(dirpath string) ([]*makeshift.Profile, error) { var ( profiles []*makeshift.Profile err error ) // walk profiles directory to load all profiles err = filepath.Walk(dirpath, func(path string, info fs.FileInfo, err error) error { if err != nil { return err } // skip directories if info.IsDir() { return nil } // read file contents var profile *makeshift.Profile profile, err = LoadProfileFromFile(path) if err != nil { return err } profiles = append(profiles, profile) fmt.Println(path, info.Size()) return nil }) if err != nil { return nil, fmt.Errorf("failed to walk directory '%s': %v", dirpath, err) } return profiles, nil } // LoadPluginFromFile loads a single plugin given a single file path func LoadPluginFromFile(path string) (makeshift.Plugin, error) { var ( isDir bool err error loadedPlugin *plugin.Plugin ) // skip loading plugin if path is a directory with no error isDir, err = util.IsDirectory(path) if err != nil { return nil, fmt.Errorf("failed to test if plugin path is directory: %v", err) } else if isDir { return nil, fmt.Errorf("path is a directory") } // 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("Makeshift") 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 makeshift.Plugin target, ok := symbol.(makeshift.Plugin) if !ok { return nil, fmt.Errorf("failed to assert 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]makeshift.Plugin, error) { // check if verbose option is supplied var ( cps = make(map[string]makeshift.Plugin) err error ) // helper to check for valid extensions var hasValidExt = func(path string) bool { return slices.Contains([]string{".so", ".dylib", ".dll"}, 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 // only try loading if file has .so extension if info.IsDir() || err != nil || !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 *makeshift.Profile) error { return saveToJSONFile(path, profile) } func SavePluginToFile(path string, plugin *makeshift.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 file: %v", err) } err = json.Unmarshal(contents, &res) if err != nil { return nil, fmt.Errorf("failed to unmarshal contents from JSON: %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 + RELPATH_PROFILES + "/" + id + ".json" } func (s *Service) PathForPluginWithName(name string) string { return s.RootPath + RELPATH_PLUGINS + "/" + name + ".so" } func (s *Service) PathForProfiles() string { return s.RootPath + RELPATH_PROFILES + "/" } func (s *Service) PathForPlugins() string { return s.RootPath + RELPATH_PLUGINS + "/" } func (s *Service) PathForData() string { return s.RootPath + RELPATH_DATA } func (s *Service) PathForMetadata() string { return s.RootPath + RELPATH_METADATA } func (s *Service) PathForHome() string { return s.RootPath + RELPATH_HELP } func (s *Service) writeErrorResponse(w http.ResponseWriter, message string, code int) { http.Error(w, message, code) log.Error().Msg(message) }