diff --git a/src/api.go b/src/api.go index 0991823..52f6499 100644 --- a/src/api.go +++ b/src/api.go @@ -234,6 +234,9 @@ func RegisterPluginAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/plugins/groups/add", pluginManager.HandleAddPluginToGroup) authRouter.HandleFunc("/api/plugins/groups/remove", pluginManager.HandleRemovePluginFromGroup) authRouter.HandleFunc("/api/plugins/groups/deleteTag", pluginManager.HandleRemovePluginGroup) + + authRouter.HandleFunc("/api/plugins/store/list", pluginManager.HandleListDownloadablePlugins) + authRouter.HandleFunc("/api/plugins/store/resync", pluginManager.HandleResyncPluginList) } // Register the APIs for Auth functions, due to scoping issue some functions are defined here diff --git a/src/mod/plugins/handler.go b/src/mod/plugins/handler.go index 8e95805..93930ab 100644 --- a/src/mod/plugins/handler.go +++ b/src/mod/plugins/handler.go @@ -249,3 +249,5 @@ func (m *Manager) HandleDisablePlugin(w http.ResponseWriter, r *http.Request) { utils.SendOK(w) } + +/* Plugin Store */ diff --git a/src/mod/plugins/plugins.go b/src/mod/plugins/plugins.go index bb4c64b..5971ea6 100644 --- a/src/mod/plugins/plugins.go +++ b/src/mod/plugins/plugins.go @@ -82,7 +82,7 @@ func (m *Manager) LoadPluginsFromDisk() error { m.Log("Loaded plugin: "+thisPlugin.Spec.Name, nil) // If the plugin was enabled, start it now - fmt.Println("Plugin enabled state", m.GetPluginPreviousEnableState(thisPlugin.Spec.ID)) + //fmt.Println("Plugin enabled state", m.GetPluginPreviousEnableState(thisPlugin.Spec.ID)) if m.GetPluginPreviousEnableState(thisPlugin.Spec.ID) { err = m.StartPlugin(thisPlugin.Spec.ID) if err != nil { diff --git a/src/mod/plugins/store.go b/src/mod/plugins/store.go new file mode 100644 index 0000000..eff3fcd --- /dev/null +++ b/src/mod/plugins/store.go @@ -0,0 +1,118 @@ +package plugins + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "imuslab.com/zoraxy/mod/plugins/zoraxy_plugin" + "imuslab.com/zoraxy/mod/utils" +) + +/* + Plugin Store +*/ + +// See https://github.com/aroz-online/zoraxy-official-plugins/blob/main/directories/index.json for the standard format + +type Checksums struct { + LinuxAmd64 string `json:"linux_amd64"` + Linux386 string `json:"linux_386"` + LinuxArm string `json:"linux_arm"` + LinuxArm64 string `json:"linux_arm64"` + LinuxMipsle string `json:"linux_mipsle"` + LinuxRiscv64 string `json:"linux_riscv64"` + WindowsAmd64 string `json:"windows_amd64"` +} + +type DownloadablePlugin struct { + IconPath string + PluginIntroSpect zoraxy_plugin.IntroSpect //Plugin introspect information + ChecksumsSHA256 Checksums //Checksums for the plugin binary + DownloadURLs map[string]string //Download URLs for different platforms +} + +/* Plugin Store Index List Sync */ +//Update the plugin list from the plugin store URLs +func (m *Manager) UpdateDownloadablePluginList() error { + //Get downloadable plugins from each of the plugin store URLS + m.Options.DownloadablePluginCache = []*DownloadablePlugin{} + for _, url := range m.Options.PluginStoreURLs { + pluginList, err := m.getPluginListFromURL(url) + if err != nil { + return fmt.Errorf("failed to get plugin list from %s: %w", url, err) + } + m.Options.DownloadablePluginCache = append(m.Options.DownloadablePluginCache, pluginList...) + } + + m.Options.LastSuccPluginSyncTime = time.Now().Unix() + + return nil +} + +// Get the plugin list from the URL +func (m *Manager) getPluginListFromURL(url string) ([]*DownloadablePlugin, error) { + //Get the plugin list from the URL + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get plugin list from %s: %s", url, resp.Status) + } + + var pluginList []*DownloadablePlugin + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read plugin list from %s: %w", url, err) + } + content = []byte(strings.TrimSpace(string(content))) + + err = json.Unmarshal(content, &pluginList) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal plugin list from %s: %w", url, err) + } + + return pluginList, nil +} + +func (m *Manager) ListDownloadablePlugins() []*DownloadablePlugin { + //List all downloadable plugins + if len(m.Options.DownloadablePluginCache) == 0 { + return []*DownloadablePlugin{} + } + return m.Options.DownloadablePluginCache +} + +/* + Handlers for Plugin Store +*/ + +func (m *Manager) HandleListDownloadablePlugins(w http.ResponseWriter, r *http.Request) { + //List all downloadable plugins + plugins := m.ListDownloadablePlugins() + js, _ := json.Marshal(plugins) + utils.SendJSONResponse(w, string(js)) +} + +// HandleResyncPluginList is the handler for resyncing the plugin list from the plugin store URLs +func (m *Manager) HandleResyncPluginList(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + //Make sure this function require csrf token + utils.SendErrorResponse(w, "Method not allowed") + return + } + + //Resync the plugin list from the plugin store URLs + err := m.UpdateDownloadablePluginList() + if err != nil { + utils.SendErrorResponse(w, "Failed to resync plugin list: "+err.Error()) + return + } + utils.SendOK(w) +} diff --git a/src/mod/plugins/store_test.go b/src/mod/plugins/store_test.go new file mode 100644 index 0000000..058648a --- /dev/null +++ b/src/mod/plugins/store_test.go @@ -0,0 +1,52 @@ +package plugins + +import ( + "testing" +) + +func TestUpdateDownloadablePluginList(t *testing.T) { + mockManager := &Manager{ + Options: &ManagerOptions{ + DownloadablePluginCache: []*DownloadablePlugin{}, + PluginStoreURLs: []string{}, + }, + } + + //Inject a mock URL for testing + mockManager.Options.PluginStoreURLs = []string{"https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json"} + + err := mockManager.UpdateDownloadablePluginList() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(mockManager.Options.DownloadablePluginCache) == 0 { + t.Fatalf("expected plugin cache to be updated, but it was empty") + } + + if mockManager.Options.LastSuccPluginSyncTime == 0 { + t.Fatalf("expected LastSuccPluginSyncTime to be updated, but it was not") + } +} + +func TestGetPluginListFromURL(t *testing.T) { + mockManager := &Manager{ + Options: &ManagerOptions{ + DownloadablePluginCache: []*DownloadablePlugin{}, + PluginStoreURLs: []string{}, + }, + } + + pluginList, err := mockManager.getPluginListFromURL("https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(pluginList) == 0 { + t.Fatalf("expected plugin list to be populated, but it was empty") + } + + for _, plugin := range pluginList { + t.Logf("Plugin: %+v", plugin) + } +} diff --git a/src/mod/plugins/typdef.go b/src/mod/plugins/typdef.go index 991651c..23ad083 100644 --- a/src/mod/plugins/typdef.go +++ b/src/mod/plugins/typdef.go @@ -29,10 +29,16 @@ type Plugin struct { } type ManagerOptions struct { + /* Plugins */ PluginDir string //The directory where the plugins are stored PluginGroups map[string][]string //The plugin groups,key is the tag name and the value is an array of plugin IDs PluginGroupsConfig string //The group / tag configuration file, if set the plugin groups will be loaded from this file + /* Plugin Downloader */ + PluginStoreURLs []string //The plugin store URLs, used to download the plugins + DownloadablePluginCache []*DownloadablePlugin //The cache for the downloadable plugins, key is the plugin ID and value is the DownloadablePlugin struct + LastSuccPluginSyncTime int64 //The last sync time for the plugin store URLs, used to check if the plugin store URLs need to be synced again + /* Runtime */ SystemConst *zoraxyPlugin.RuntimeConstantValue //The system constant value CSRFTokenGen func(*http.Request) string `json:"-"` //The CSRF token generator function diff --git a/src/start.go b/src/start.go index 2f3c79e..83ed7f1 100644 --- a/src/start.go +++ b/src/start.go @@ -1,7 +1,6 @@ package main import ( - "imuslab.com/zoraxy/mod/auth/sso/authentik" "log" "net/http" "os" @@ -10,6 +9,8 @@ import ( "strings" "time" + "imuslab.com/zoraxy/mod/auth/sso/authentik" + "github.com/gorilla/csrf" "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" @@ -322,6 +323,9 @@ func startupSequence() { ZoraxyUUID: nodeUUID, DevelopmentBuild: DEVELOPMENT_BUILD, }, + PluginStoreURLs: []string{ + "https://raw.githubusercontent.com/aroz-online/zoraxy-official-plugins/refs/heads/main/directories/index.json", + }, Database: sysdb, Logger: SystemWideLogger, PluginGroupsConfig: CONF_PLUGIN_GROUPS, @@ -330,9 +334,19 @@ func startupSequence() { }, }) + //Sync latest plugin list from the plugin store + go func() { + err = pluginManager.UpdateDownloadablePluginList() + if err != nil { + SystemWideLogger.PrintAndLog("plugin-manager", "Failed to sync plugin list from plugin store", err) + } else { + SystemWideLogger.PrintAndLog("plugin-manager", "Plugin list synced from plugin store", nil) + } + }() + err = pluginManager.LoadPluginsFromDisk() if err != nil { - SystemWideLogger.PrintAndLog("Plugin Manager", "Failed to load plugins", err) + SystemWideLogger.PrintAndLog("plugin-manager", "Failed to load plugins", err) } /* Docker UX Optimizer */ diff --git a/src/web/components/plugins.html b/src/web/components/plugins.html index bf2d9ef..bae23ba 100644 --- a/src/web/components/plugins.html +++ b/src/web/components/plugins.html @@ -185,6 +185,8 @@ +
+ diff --git a/src/web/main.css b/src/web/main.css index 713cb12..7359506 100644 --- a/src/web/main.css +++ b/src/web/main.css @@ -197,6 +197,12 @@ body.darkTheme .menubar{ max-width: calc(80% - 1em); } +@media screen and (max-width: 478px) { + .sideWrapper.extendedMode { + max-width: calc(100% - 1em); + } +} + .sideWrapper .content{ height: 100%; width: 100%; diff --git a/src/web/snippet/pluginstore.html b/src/web/snippet/pluginstore.html new file mode 100644 index 0000000..a7a4b91 --- /dev/null +++ b/src/web/snippet/pluginstore.html @@ -0,0 +1,258 @@ + + + + + + + Plugin Store + + + + + + + + + +
+
+ +
+ +
+ +
+
+
+
+ + Advance Settings +
+
+

Plugin Store URLs

+
+
+ + +
+ +
+ +
+
+
+
+
+ +
+



+
+ + + \ No newline at end of file