diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 7f794f4bcb..cdcf490a7f 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "os" + "path/filepath" "strings" "testing" "time" @@ -63,11 +64,24 @@ func UseTestStore(store store.Store) { func setupTestHelper(enterprise bool, updateConfig func(*model.Config)) *TestHelper { testStore.DropAllTables() + tempWorkspace, err := ioutil.TempDir("", "apptest") + if err != nil { + panic(err) + } + memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true}) if err != nil { panic("failed to initialize memory store: " + err.Error()) } + config := memoryStore.Get() + *config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins") + *config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp") + if updateConfig != nil { + updateConfig(config) + } + memoryStore.Set(config) + var options []app.Option options = append(options, app.ConfigStore(memoryStore)) options = append(options, app.StoreOverride(testStore)) @@ -94,9 +108,6 @@ func setupTestHelper(enterprise bool, updateConfig func(*model.Config)) *TestHel }) prevListenAddress := *th.App.Config().ServiceSettings.ListenAddress th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" }) - if updateConfig != nil { - th.App.UpdateConfig(updateConfig) - } serverErr := th.Server.Start() if serverErr != nil { panic(serverErr) @@ -130,11 +141,7 @@ func setupTestHelper(enterprise bool, updateConfig func(*model.Config)) *TestHel th.SystemAdminClient = th.CreateClient() if th.tempWorkspace == "" { - dir, err := ioutil.TempDir("", "apptest") - if err != nil { - panic(err) - } - th.tempWorkspace = dir + th.tempWorkspace = tempWorkspace } return th @@ -580,9 +587,7 @@ func CheckEtag(t *testing.T, data interface{}, resp *model.Response) { func CheckNoError(t *testing.T, resp *model.Response) { t.Helper() - if resp.Error != nil { - require.FailNow(t, "Expected no error, got %q", resp.Error.Error()) - } + require.Nil(t, resp.Error, "expected no error") } func checkHTTPStatus(t *testing.T, resp *model.Response, expectedStatus int, expectError bool) { diff --git a/api4/plugin.go b/api4/plugin.go index 6e74ec2d1f..74a7c734ec 100644 --- a/api4/plugin.go +++ b/api4/plugin.go @@ -9,10 +9,9 @@ import ( "bytes" "encoding/json" "io" - "io/ioutil" "net/http" "net/url" - "time" + "strconv" "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" @@ -20,9 +19,6 @@ import ( const ( MAXIMUM_PLUGIN_FILE_SIZE = 50 * 1024 * 1024 - // INSTALL_PLUGIN_FROM_URL_HTTP_REQUEST_TIMEOUT defines a high timeout for installing plugins - // from an external URL to avoid slow connections or large plugins from failing to install. - INSTALL_PLUGIN_FROM_URL_HTTP_REQUEST_TIMEOUT = 60 * time.Minute ) func (api *API) InitPlugin() { @@ -100,15 +96,15 @@ func installPluginFromUrl(c *Context, w http.ResponseWriter, r *http.Request) { } force := r.URL.Query().Get("force") == "true" - downloadUrl := r.URL.Query().Get("plugin_download_url") + downloadURL := r.URL.Query().Get("plugin_download_url") - pluginFile, err := downloadFromUrl(c, downloadUrl) + pluginFileBytes, err := c.App.DownloadFromURL(downloadURL) if err != nil { - c.Err = err + c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest) return } - installPlugin(c, w, pluginFile, force) + installPlugin(c, w, bytes.NewReader(pluginFileBytes), force) } func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request) { @@ -132,28 +128,13 @@ func installMarketplacePlugin(c *Context, w http.ResponseWriter, r *http.Request c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.marketplace_plugin_request.app_error", nil, err.Error(), http.StatusNotImplemented) return } - plugin, appErr := c.App.GetMarketplacePlugin(pluginRequest) + + manifest, appErr := c.App.InstallMarketplacePlugin(pluginRequest) if appErr != nil { c.Err = appErr return } - pluginFile, appErr := downloadFromUrl(c, plugin.DownloadURL) - if appErr != nil { - c.Err = appErr - return - } - signature, err := plugin.DecodeSignature() - if err != nil { - c.Err = model.NewAppError("installMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, err.Error(), http.StatusNotImplemented) - return - } - - manifest, appErr := c.App.InstallPluginWithSignature(pluginFile, signature) - if appErr != nil { - c.Err = appErr - return - } w.WriteHeader(http.StatusCreated) w.Write([]byte(manifest.ToJson())) } @@ -347,45 +328,20 @@ func parseMarketplacePluginFilter(u *url.URL) (*model.MarketplacePluginFilter, e filter := u.Query().Get("filter") serverVersion := u.Query().Get("server_version") + localOnly, err := strconv.ParseBool(u.Query().Get("local_only")) + if err != nil { + localOnly = false + } return &model.MarketplacePluginFilter{ Page: page, PerPage: perPage, Filter: filter, ServerVersion: serverVersion, + LocalOnly: localOnly, }, nil } -func downloadFromUrl(c *Context, downloadUrl string) (io.ReadSeeker, *model.AppError) { - if !model.IsValidHttpUrl(downloadUrl) { - return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.invalid_url.app_error", nil, "", http.StatusBadRequest) - } - - u, err := url.ParseRequestURI(downloadUrl) - if err != nil { - return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.invalid_url.app_error", nil, "", http.StatusBadRequest) - } - if !*c.App.Config().PluginSettings.AllowInsecureDownloadUrl && u.Scheme != "https" { - return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.insecure_url.app_error", nil, "", http.StatusBadRequest) - } - - client := c.App.HTTPService.MakeClient(true) - client.Timeout = INSTALL_PLUGIN_FROM_URL_HTTP_REQUEST_TIMEOUT - - resp, err := client.Get(downloadUrl) - if err != nil { - return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest) - } - defer resp.Body.Close() - - fileBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, model.NewAppError("downloadFromUrl", "api.plugin.install.reading_stream_failed.app_error", nil, err.Error(), http.StatusBadRequest) - } - - return bytes.NewReader(fileBytes), nil -} - func installPlugin(c *Context, w http.ResponseWriter, plugin io.ReadSeeker, force bool) { manifest, appErr := c.App.InstallPlugin(plugin, force) if appErr != nil { diff --git a/api4/plugin_test.go b/api4/plugin_test.go index 8ac031bff8..eee0d1e5e0 100644 --- a/api4/plugin_test.go +++ b/api4/plugin_test.go @@ -18,8 +18,11 @@ import ( "time" "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/plugin" "github.com/mattermost/mattermost-server/v5/testlib" + "github.com/mattermost/mattermost-server/v5/utils" "github.com/mattermost/mattermost-server/v5/utils/fileutils" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -61,6 +64,10 @@ func TestPlugin(t *testing.T) { CheckNoError(t, resp) assert.Equal(t, "testplugin", manifest.Id) + ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id) + CheckNoError(t, resp) + require.True(t, ok) + t.Run("install plugin from URL with slow response time", func(t *testing.T) { if testing.Short() { t.Skip("skipping test to install plugin from a slow response server") @@ -163,7 +170,7 @@ func TestPlugin(t *testing.T) { assert.False(t, found) // Successful activate - ok, resp := th.SystemAdminClient.EnablePlugin(manifest.Id) + ok, resp = th.SystemAdminClient.EnablePlugin(manifest.Id) CheckNoError(t, resp) assert.True(t, ok) @@ -602,7 +609,7 @@ func TestGetInstalledMarketplacePlugins(t *testing.T) { DownloadURL: "", Labels: []model.MarketplaceLabel{{ Name: "Local", - Description: "This plugin is not listed in the marketplace but was installed manually", + Description: "This plugin is not listed in the marketplace", }}, Manifest: manifest, }, @@ -673,7 +680,7 @@ func TestGetInstalledMarketplacePlugins(t *testing.T) { plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{}) CheckNoError(t, resp) - newPlugin.InstalledVersion = manifest.Version + newPlugin.InstalledVersion = "" require.Equal(t, expectedPlugins, plugins) }) } @@ -701,7 +708,7 @@ func TestSearchGetMarketplacePlugins(t *testing.T) { tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz")) require.NoError(t, err) - tarDataV2, err := ioutil.ReadFile(filepath.Join(path, "testpluginv2.tar.gz")) + tarDataV2, err := ioutil.ReadFile(filepath.Join(path, "testplugin2.tar.gz")) require.NoError(t, err) t.Run("search installed plugin", func(t *testing.T) { @@ -730,37 +737,37 @@ func TestSearchGetMarketplacePlugins(t *testing.T) { manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) CheckNoError(t, resp) - newPluginV1 := &model.MarketplacePlugin{ + plugin1 := &model.MarketplacePlugin{ BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ HomepageURL: "", IconData: "", DownloadURL: "", Labels: []model.MarketplaceLabel{{ Name: "Local", - Description: "This plugin is not listed in the marketplace but was installed manually", + Description: "This plugin is not listed in the marketplace", }}, Manifest: manifest, }, InstalledVersion: manifest.Version, } - expectedPlugins := append(samplePlugins, newPluginV1) + expectedPlugins := append(samplePlugins, plugin1) manifest, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarDataV2)) CheckNoError(t, resp) - newPluginV2 := &model.MarketplacePlugin{ + plugin2 := &model.MarketplacePlugin{ BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ HomepageURL: "", IconData: "", DownloadURL: "", Labels: []model.MarketplaceLabel{{ Name: "Local", - Description: "This plugin is not listed in the marketplace but was installed manually", + Description: "This plugin is not listed in the marketplace", }}, Manifest: manifest, }, InstalledVersion: manifest.Version, } - expectedPlugins = append(expectedPlugins, newPluginV2) + expectedPlugins = append(expectedPlugins, plugin2) sort.SliceStable(expectedPlugins, func(i, j int) bool { return strings.ToLower(expectedPlugins[i].Manifest.Name) < strings.ToLower(expectedPlugins[j].Manifest.Name) }) @@ -770,13 +777,13 @@ func TestSearchGetMarketplacePlugins(t *testing.T) { require.Equal(t, expectedPlugins, plugins) // Search for plugins from the server - plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "testplugin_v2"}) + plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "testplugin2"}) CheckNoError(t, resp) - require.Equal(t, []*model.MarketplacePlugin{newPluginV2}, plugins) + require.Equal(t, []*model.MarketplacePlugin{plugin2}, plugins) - plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "dsgsdg_v2"}) + plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "a second plugin"}) CheckNoError(t, resp) - require.Equal(t, []*model.MarketplacePlugin{newPluginV2}, plugins) + require.Equal(t, []*model.MarketplacePlugin{plugin2}, plugins) plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "User Satisfaction Surveys"}) CheckNoError(t, resp) @@ -785,6 +792,270 @@ func TestSearchGetMarketplacePlugins(t *testing.T) { plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{Filter: "NOFILTER"}) CheckNoError(t, resp) require.Nil(t, plugins) + + // cleanup + ok, resp := th.SystemAdminClient.RemovePlugin(plugin1.Manifest.Id) + CheckNoError(t, resp) + assert.True(t, ok) + + ok, resp = th.SystemAdminClient.RemovePlugin(plugin2.Manifest.Id) + CheckNoError(t, resp) + assert.True(t, ok) + }) +} + +func TestGetLocalPluginInMarketplace(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + samplePlugins := []*model.MarketplacePlugin{ + { + BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ + HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps", + IconData: "https://example.com/icon.svg", + DownloadURL: "www.github.com/example", + Manifest: &model.Manifest{ + Id: "testplugin2", + Name: "testplugin2", + Description: "a second plugin", + Version: "1.2.2", + MinServerVersion: "", + }, + }, + InstalledVersion: "", + }, + } + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[0]}) + require.NoError(t, err) + res.Write(json) + })) + defer testServer.Close() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.EnableMarketplace = true + *cfg.PluginSettings.MarketplaceUrl = testServer.URL + }) + + t.Run("Get plugins with EnableRemoteMarketplace enabled", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableRemoteMarketplace = true + }) + + plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{}) + CheckNoError(t, resp) + + require.Len(t, plugins, len(samplePlugins)) + require.Equal(t, samplePlugins, plugins) + }) + + t.Run("get remote and local plugins", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableRemoteMarketplace = true + *cfg.PluginSettings.EnableUploads = true + }) + + // Upload one local plugin + path, _ := fileutils.FindDir("tests") + tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz")) + require.NoError(t, err) + + manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) + CheckNoError(t, resp) + + plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{}) + CheckNoError(t, resp) + + require.Len(t, plugins, 2) + + ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id) + CheckNoError(t, resp) + assert.True(t, ok) + }) + + t.Run("EnableRemoteMarketplace disabled", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableRemoteMarketplace = false + *cfg.PluginSettings.EnableUploads = true + }) + + // No marketplace plugins returned + plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{}) + CheckNoError(t, resp) + + require.Len(t, plugins, 0) + + // Upload one local plugin + path, _ := fileutils.FindDir("tests") + tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz")) + require.NoError(t, err) + + manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) + CheckNoError(t, resp) + + newPlugin := &model.MarketplacePlugin{ + BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ + Manifest: manifest, + }, + InstalledVersion: manifest.Version, + } + + plugins, resp = th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{}) + CheckNoError(t, resp) + + // Only get the local plugins + require.Len(t, plugins, 1) + require.Equal(t, newPlugin, plugins[0]) + + ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id) + CheckNoError(t, resp) + assert.True(t, ok) + }) + + t.Run("local_only true", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableRemoteMarketplace = true + *cfg.PluginSettings.EnableUploads = true + }) + + // Upload one local plugin + path, _ := fileutils.FindDir("tests") + tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin.tar.gz")) + require.NoError(t, err) + + manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) + CheckNoError(t, resp) + + newPlugin := &model.MarketplacePlugin{ + BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ + Manifest: manifest, + Labels: []model.MarketplaceLabel{{ + Name: "Local", + Description: "This plugin is not listed in the marketplace", + }}, + }, + InstalledVersion: manifest.Version, + } + + plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{LocalOnly: true}) + CheckNoError(t, resp) + + require.Len(t, plugins, 1) + require.Equal(t, newPlugin, plugins[0]) + + ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id) + CheckNoError(t, resp) + assert.True(t, ok) + }) +} + +func TestGetPrepackagedPluginInMarketplace(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + marketplacePlugins := []*model.MarketplacePlugin{ + { + BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ + HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps", + IconData: "https://example.com/icon.svg", + DownloadURL: "www.github.com/example", + Manifest: &model.Manifest{ + Id: "marketplace.test", + Name: "marketplacetest", + Description: "a marketplace plugin", + Version: "0.1.2", + MinServerVersion: "", + }, + }, + InstalledVersion: "", + }, + } + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + json, err := json.Marshal([]*model.MarketplacePlugin{marketplacePlugins[0]}) + require.NoError(t, err) + res.Write(json) + })) + defer testServer.Close() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.EnableMarketplace = true + *cfg.PluginSettings.MarketplaceUrl = testServer.URL + }) + + prepackagePlugin := &plugin.PrepackagedPlugin{ + Manifest: &model.Manifest{ + Version: "0.0.1", + Id: "prepackaged.test", + }, + } + + env := th.App.GetPluginsEnvironment() + env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin}) + + t.Run("get remote and prepackaged plugins", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableRemoteMarketplace = true + *cfg.PluginSettings.EnableUploads = true + }) + + plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{}) + CheckNoError(t, resp) + + expectedPlugins := marketplacePlugins + expectedPlugins = append(expectedPlugins, &model.MarketplacePlugin{ + BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ + Manifest: prepackagePlugin.Manifest, + }, + }) + + require.ElementsMatch(t, expectedPlugins, plugins) + require.Len(t, plugins, 2) + }) + + t.Run("EnableRemoteMarketplace disabled", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableRemoteMarketplace = false + *cfg.PluginSettings.EnableUploads = true + }) + + // No marketplace plugins returned + plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{}) + CheckNoError(t, resp) + + // Only returns the prepackaged plugins + require.Len(t, plugins, 1) + require.Equal(t, prepackagePlugin.Manifest, plugins[0].Manifest) + }) + + t.Run("get prepackaged plugin if newer", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableRemoteMarketplace = true + *cfg.PluginSettings.EnableUploads = true + }) + + manifest := &model.Manifest{ + Version: "1.2.3", + Id: "marketplace.test", + } + + newerPrepackagePlugin := &plugin.PrepackagedPlugin{ + Manifest: manifest, + } + + env := th.App.GetPluginsEnvironment() + env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{newerPrepackagePlugin}) + + plugins, resp := th.SystemAdminClient.GetMarketplacePlugins(&model.MarketplacePluginFilter{}) + CheckNoError(t, resp) + + require.Len(t, plugins, 1) + require.Equal(t, newerPrepackagePlugin.Manifest, plugins[0].Manifest) }) } @@ -797,15 +1068,16 @@ func TestInstallMarketplacePlugin(t *testing.T) { *cfg.PluginSettings.EnableUploads = true *cfg.PluginSettings.EnableMarketplace = false }) + path, _ := fileutils.FindDir("tests") - signatureFilename := "testpluginv2.tar.gz.sig" + signatureFilename := "testplugin2.tar.gz.sig" signatureFileReader, err := os.Open(filepath.Join(path, signatureFilename)) require.Nil(t, err) sigFile, err := ioutil.ReadAll(signatureFileReader) require.Nil(t, err) pluginSignature := base64.StdEncoding.EncodeToString(sigFile) - tarData, err := ioutil.ReadFile(filepath.Join(path, "testpluginv2.tar.gz")) + tarData, err := ioutil.ReadFile(filepath.Join(path, "testplugin2.tar.gz")) require.NoError(t, err) pluginServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusOK) @@ -820,9 +1092,9 @@ func TestInstallMarketplacePlugin(t *testing.T) { IconData: "https://example.com/icon.svg", DownloadURL: pluginServer.URL, Manifest: &model.Manifest{ - Id: "testplugin_v2", - Name: "testplugin_v2", - Description: "dsgsdg_v2", + Id: "testplugin2", + Name: "testplugin2", + Description: "a second plugin", Version: "1.2.2", MinServerVersion: "", }, @@ -835,9 +1107,9 @@ func TestInstallMarketplacePlugin(t *testing.T) { IconData: "https://example.com/icon.svg", DownloadURL: pluginServer.URL, Manifest: &model.Manifest{ - Id: "testplugin_v2", - Name: "testplugin_v2", - Description: "dsgsdg_v2", + Id: "testplugin2", + Name: "testplugin2", + Description: "a second plugin", Version: "1.2.3", MinServerVersion: "", }, @@ -846,7 +1118,9 @@ func TestInstallMarketplacePlugin(t *testing.T) { InstalledVersion: "", }, } + request := &model.InstallMarketplacePluginRequest{Id: "", Version: ""} + t.Run("marketplace disabled", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.EnableMarketplace = false @@ -856,6 +1130,7 @@ func TestInstallMarketplacePlugin(t *testing.T) { CheckNotImplementedStatus(t, resp) require.Nil(t, plugin) }) + t.Run("RequirePluginSignature enabled", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true @@ -925,7 +1200,7 @@ func TestInstallMarketplacePlugin(t *testing.T) { *cfg.PluginSettings.MarketplaceUrl = testServer.URL *cfg.PluginSettings.AllowInsecureDownloadUrl = true }) - pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin_v2", Version: "1.2.2"} + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "1.2.2"} plugin, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest) CheckInternalErrorStatus(t, resp) require.Nil(t, plugin) @@ -945,6 +1220,7 @@ func TestInstallMarketplacePlugin(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.EnableMarketplace = true + *cfg.PluginSettings.EnableRemoteMarketplace = true *cfg.PluginSettings.MarketplaceUrl = testServer.URL }) @@ -953,14 +1229,14 @@ func TestInstallMarketplacePlugin(t *testing.T) { appErr := th.App.AddPublicKey("pub_key", key) require.Nil(t, appErr) - pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin_v2", Version: "1.2.3"} + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "1.2.3"} manifest, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest) CheckNoError(t, resp) require.NotNil(t, manifest) - require.Equal(t, "testplugin_v2", manifest.Id) + require.Equal(t, "testplugin2", manifest.Id) require.Equal(t, "1.2.3", manifest.Version) - filePath := filepath.Join(*th.App.Config().PluginSettings.Directory, "testplugin_v2.tar.gz.sig") + filePath := filepath.Join("plugins", "testplugin2.tar.gz.sig") savedSigFile, err := th.App.ReadFile(filePath) require.Nil(t, err) require.EqualValues(t, sigFile, savedSigFile) @@ -975,6 +1251,221 @@ func TestInstallMarketplacePlugin(t *testing.T) { appErr = th.App.DeletePublicKey("pub_key") require.Nil(t, appErr) }) + + t.Run("install prepackaged and remote plugins through marketplace", func(t *testing.T) { + prepackagedPluginsDir := "prepackaged_plugins" + + os.RemoveAll(prepackagedPluginsDir) + err := os.Mkdir(prepackagedPluginsDir, os.ModePerm) + require.NoError(t, err) + defer os.RemoveAll(prepackagedPluginsDir) + + prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsDir) + require.True(t, found, "failed to find prepackaged plugins directory") + + err = utils.CopyFile(filepath.Join(path, "testplugin.tar.gz"), filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz")) + require.NoError(t, err) + err = utils.CopyFile(filepath.Join(path, "testplugin.tar.gz.asc"), filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig")) + require.NoError(t, err) + + th := SetupConfig(func(cfg *model.Config) { + // Disable auto-installing prepackaged plugins + *cfg.PluginSettings.AutomaticPrepackagedPlugins = false + }).InitBasic() + defer th.TearDown() + + pluginSignatureFile, err := os.Open(filepath.Join(path, "testplugin.tar.gz.asc")) + require.Nil(t, err) + pluginSignatureData, err := ioutil.ReadAll(pluginSignatureFile) + require.Nil(t, err) + + key, err := os.Open(filepath.Join(path, "development-private-key.asc")) + require.NoError(t, err) + appErr := th.App.AddPublicKey("pub_key", key) + require.Nil(t, appErr) + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + serverVersion := req.URL.Query().Get("server_version") + require.NotEmpty(t, serverVersion) + require.Equal(t, model.CurrentVersion, serverVersion) + res.WriteHeader(http.StatusOK) + json, err := json.Marshal([]*model.MarketplacePlugin{samplePlugins[1]}) + require.NoError(t, err) + res.Write(json) + })) + defer testServer.Close() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableMarketplace = true + *cfg.PluginSettings.EnableRemoteMarketplace = false + *cfg.PluginSettings.MarketplaceUrl = testServer.URL + *cfg.PluginSettings.AllowInsecureDownloadUrl = false + }) + + env := th.App.GetPluginsEnvironment() + + pluginsResp, resp := th.SystemAdminClient.GetPlugins() + CheckNoError(t, resp) + require.Len(t, pluginsResp.Active, 0) + require.Len(t, pluginsResp.Inactive, 0) + + // Should fail to install unknown prepackaged plugin + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin", Version: "0.0.2"} + manifest, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest) + CheckInternalErrorStatus(t, resp) + require.Nil(t, manifest) + + plugins := env.PrepackagedPlugins() + require.Len(t, plugins, 1) + require.Equal(t, "testplugin", plugins[0].Manifest.Id) + require.Equal(t, pluginSignatureData, plugins[0].Signature) + + pluginsResp, resp = th.SystemAdminClient.GetPlugins() + CheckNoError(t, resp) + require.Len(t, pluginsResp.Active, 0) + require.Len(t, pluginsResp.Inactive, 0) + + pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin", Version: "0.0.1"} + manifest1, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest) + CheckNoError(t, resp) + require.NotNil(t, manifest1) + require.Equal(t, "testplugin", manifest1.Id) + require.Equal(t, "0.0.1", manifest1.Version) + + pluginsResp, resp = th.SystemAdminClient.GetPlugins() + CheckNoError(t, resp) + require.Len(t, pluginsResp.Active, 0) + require.Equal(t, pluginsResp.Inactive, []*model.PluginInfo{{ + Manifest: *manifest1, + }}) + + // Try to install remote marketplace plugin + pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "1.2.3"} + manifest, resp = th.SystemAdminClient.InstallMarketplacePlugin(pRequest) + CheckInternalErrorStatus(t, resp) + require.Nil(t, manifest) + + // Enable remote marketplace + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableMarketplace = true + *cfg.PluginSettings.EnableRemoteMarketplace = true + *cfg.PluginSettings.MarketplaceUrl = testServer.URL + *cfg.PluginSettings.AllowInsecureDownloadUrl = true + }) + + pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "1.2.3"} + manifest2, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest) + CheckNoError(t, resp) + require.NotNil(t, manifest2) + require.Equal(t, "testplugin2", manifest2.Id) + require.Equal(t, "1.2.3", manifest2.Version) + + pluginsResp, resp = th.SystemAdminClient.GetPlugins() + CheckNoError(t, resp) + require.Len(t, pluginsResp.Active, 0) + require.ElementsMatch(t, pluginsResp.Inactive, []*model.PluginInfo{ + { + Manifest: *manifest1, + }, + { + Manifest: *manifest2, + }, + }) + + // Clean up + ok, resp := th.SystemAdminClient.RemovePlugin(manifest1.Id) + CheckNoError(t, resp) + assert.True(t, ok) + + ok, resp = th.SystemAdminClient.RemovePlugin(manifest2.Id) + CheckNoError(t, resp) + assert.True(t, ok) + + appErr = th.App.DeletePublicKey("pub_key") + require.Nil(t, appErr) + }) + + t.Run("missing prepackaged and remote plugin signatures", func(t *testing.T) { + prepackagedPluginsDir := "prepackaged_plugins" + + os.RemoveAll(prepackagedPluginsDir) + err := os.Mkdir(prepackagedPluginsDir, os.ModePerm) + require.NoError(t, err) + defer os.RemoveAll(prepackagedPluginsDir) + + prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsDir) + require.True(t, found, "failed to find prepackaged plugins directory") + + err = utils.CopyFile(filepath.Join(path, "testplugin.tar.gz"), filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz")) + require.NoError(t, err) + + th := SetupConfig(func(cfg *model.Config) { + // Disable auto-installing prepackged plugins + *cfg.PluginSettings.AutomaticPrepackagedPlugins = false + }).InitBasic() + defer th.TearDown() + + key, err := os.Open(filepath.Join(path, "development-private-key.asc")) + require.NoError(t, err) + appErr := th.App.AddPublicKey("pub_key", key) + require.Nil(t, appErr) + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + serverVersion := req.URL.Query().Get("server_version") + require.NotEmpty(t, serverVersion) + require.Equal(t, model.CurrentVersion, serverVersion) + + mPlugins := []*model.MarketplacePlugin{samplePlugins[0]} + require.Empty(t, mPlugins[0].Signature) + res.WriteHeader(http.StatusOK) + json, err := json.Marshal(mPlugins) + require.NoError(t, err) + res.Write(json) + })) + defer testServer.Close() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableMarketplace = true + *cfg.PluginSettings.EnableRemoteMarketplace = true + *cfg.PluginSettings.MarketplaceUrl = testServer.URL + *cfg.PluginSettings.AllowInsecureDownloadUrl = true + }) + + env := th.App.GetPluginsEnvironment() + plugins := env.PrepackagedPlugins() + require.Len(t, plugins, 1) + require.Equal(t, "testplugin", plugins[0].Manifest.Id) + require.Empty(t, plugins[0].Signature) + + pluginsResp, resp := th.SystemAdminClient.GetPlugins() + CheckNoError(t, resp) + require.Len(t, pluginsResp.Active, 0) + require.Len(t, pluginsResp.Inactive, 0) + + pRequest := &model.InstallMarketplacePluginRequest{Id: "testplugin", Version: "0.0.1"} + manifest, resp := th.SystemAdminClient.InstallMarketplacePlugin(pRequest) + CheckInternalErrorStatus(t, resp) + require.Nil(t, manifest) + + pluginsResp, resp = th.SystemAdminClient.GetPlugins() + CheckNoError(t, resp) + require.Len(t, pluginsResp.Active, 0) + require.Len(t, pluginsResp.Inactive, 0) + + pRequest = &model.InstallMarketplacePluginRequest{Id: "testplugin2", Version: "1.2.3"} + manifest, resp = th.SystemAdminClient.InstallMarketplacePlugin(pRequest) + CheckInternalErrorStatus(t, resp) + require.Nil(t, manifest) + + pluginsResp, resp = th.SystemAdminClient.GetPlugins() + CheckNoError(t, resp) + require.Len(t, pluginsResp.Active, 0) + require.Len(t, pluginsResp.Inactive, 0) + + // Clean up + appErr = th.App.DeletePublicKey("pub_key") + require.Nil(t, appErr) + }) } func findClusterMessages(event string, msgs []*model.ClusterMessage) []*model.ClusterMessage { diff --git a/app/download.go b/app/download.go new file mode 100644 index 0000000000..31d137fef2 --- /dev/null +++ b/app/download.go @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "io/ioutil" + "net/url" + "time" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/pkg/errors" +) + +const ( + // HTTP_REQUEST_TIMEOUT defines a high timeout for downloading large files + // from an external URL to avoid slow connections from failing to install. + HTTP_REQUEST_TIMEOUT = 1 * time.Hour +) + +func (a *App) DownloadFromURL(downloadURL string) ([]byte, error) { + if !model.IsValidHttpUrl(downloadURL) { + return nil, errors.Errorf("invalid url %s", downloadURL) + } + + u, err := url.ParseRequestURI(downloadURL) + if err != nil { + return nil, errors.Errorf("failed to parse url %s", downloadURL) + } + if !*a.Config().PluginSettings.AllowInsecureDownloadUrl && u.Scheme != "https" { + return nil, errors.Errorf("insecure url not allowed %s", downloadURL) + } + + client := a.HTTPService.MakeClient(true) + client.Timeout = HTTP_REQUEST_TIMEOUT + + resp, err := client.Get(downloadURL) + if err != nil { + return nil, errors.Wrapf(err, "failed to fetch from %s", downloadURL) + } + defer resp.Body.Close() + + return ioutil.ReadAll(resp.Body) +} diff --git a/app/helper_test.go b/app/helper_test.go index f758c7883c..53435da6d6 100644 --- a/app/helper_test.go +++ b/app/helper_test.go @@ -37,11 +37,21 @@ func setupTestHelper(enterprise bool, tb testing.TB) *TestHelper { store := mainHelper.GetStore() store.DropAllTables() + tempWorkspace, err := ioutil.TempDir("", "apptest") + if err != nil { + panic(err) + } + memoryStore, err := config.NewMemoryStoreWithOptions(&config.MemoryStoreOptions{IgnoreEnvironmentOverrides: true}) if err != nil { panic("failed to initialize memory store: " + err.Error()) } + config := memoryStore.Get() + *config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins") + *config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp") + memoryStore.Set(config) + var options []Option options = append(options, ConfigStore(memoryStore)) options = append(options, StoreOverride(mainHelper.Store)) @@ -88,18 +98,9 @@ func setupTestHelper(enterprise bool, tb testing.TB) *TestHelper { } if th.tempWorkspace == "" { - dir, err := ioutil.TempDir("", "apptest") - if err != nil { - panic(err) - } - th.tempWorkspace = dir + th.tempWorkspace = tempWorkspace } - pluginDir := filepath.Join(th.tempWorkspace, "plugins") - webappDir := filepath.Join(th.tempWorkspace, "webapp") - - th.App.InitPlugins(pluginDir, webappDir) - return th } diff --git a/app/plugin.go b/app/plugin.go index 5ab44d5927..3362e84b32 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -4,6 +4,10 @@ package app import ( + "encoding/base64" + "fmt" + "io" + "io/ioutil" "net/http" "os" "path/filepath" @@ -16,9 +20,14 @@ import ( "github.com/mattermost/mattermost-server/v5/services/filesstore" "github.com/mattermost/mattermost-server/v5/services/marketplace" "github.com/mattermost/mattermost-server/v5/utils/fileutils" + + "github.com/blang/semver" + svg "github.com/h2non/go-is-svg" "github.com/pkg/errors" ) +const prepackagedPluginsDir = "prepackaged_plugins" + type pluginSignaturePath struct { pluginId string path string @@ -163,32 +172,9 @@ func (a *App) InitPlugins(pluginDir, webappPluginDir string) { mlog.Error("Failed to sync plugins from the file store", mlog.Err(err)) } - prepackagedPluginsDir, found := fileutils.FindDir("prepackaged_plugins") - if found { - if err := filepath.Walk(prepackagedPluginsDir, func(walkPath string, info os.FileInfo, err error) error { - if !strings.HasSuffix(walkPath, ".tar.gz") { - return nil - } - - fileReader, err := os.Open(walkPath) - if err != nil { - mlog.Error("Failed to open prepackaged plugin", mlog.Err(err), mlog.String("path", walkPath)) - return nil - } - defer fileReader.Close() - - mlog.Debug("Installing prepackaged plugin", mlog.String("path", walkPath)) - - _, appErr := a.installPluginLocally(fileReader, nil, installPluginLocallyOnlyIfNewOrUpgrade) - if appErr != nil { - mlog.Error("Failed to unpack prepackaged plugin", mlog.Err(appErr), mlog.String("path", walkPath)) - } - - return nil - }); err != nil { - mlog.Error("Failed to complete unpacking prepackaged plugins", mlog.Err(err)) - } - } + plugins := a.processPrepackagedPlugins(prepackagedPluginsDir) + pluginsEnvironment = a.GetPluginsEnvironment() + pluginsEnvironment.SetPrepackagedPlugins(plugins) // Sync plugin active state when config changes. Also notify plugins. a.Srv.PluginsLock.Lock() @@ -319,7 +305,7 @@ func (a *App) EnablePlugin(id string) *model.AppError { return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - plugins, err := pluginsEnvironment.Available() + availablePlugins, err := pluginsEnvironment.Available() if err != nil { return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -327,7 +313,7 @@ func (a *App) EnablePlugin(id string) *model.AppError { id = strings.ToLower(id) var manifest *model.Manifest - for _, p := range plugins { + for _, p := range availablePlugins { if p.Manifest.Id == id { manifest = p.Manifest break @@ -361,7 +347,7 @@ func (a *App) DisablePlugin(id string) *model.AppError { return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } - plugins, err := pluginsEnvironment.Available() + availablePlugins, err := pluginsEnvironment.Available() if err != nil { return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -369,7 +355,7 @@ func (a *App) DisablePlugin(id string) *model.AppError { id = strings.ToLower(id) var manifest *model.Manifest - for _, p := range plugins { + for _, p := range availablePlugins { if p.Manifest.Id == id { manifest = p.Manifest break @@ -423,94 +409,35 @@ func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) { return resp, nil } -// GetMarketplacePlugin returns plugin from marketplace-server -func (a *App) GetMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.BaseMarketplacePlugin, *model.AppError) { - marketplaceClient, err := marketplace.NewClient( - *a.Config().PluginSettings.MarketplaceUrl, - a.HTTPService, - ) - if err != nil { - return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - filter := &model.MarketplacePluginFilter{Filter: request.Id, ServerVersion: model.CurrentVersion} - plugin, err := marketplaceClient.GetPlugin(filter, request.Version) - if err != nil { - return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, err.Error(), http.StatusInternalServerError) - } - return plugin, nil -} - // GetMarketplacePlugins returns a list of plugins from the marketplace-server, // and plugins that are installed locally. func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) { + plugins := map[string]*model.MarketplacePlugin{} + + if *a.Config().PluginSettings.EnableRemoteMarketplace && !filter.LocalOnly { + p, appErr := a.getRemotePlugins(filter) + if appErr != nil { + return nil, appErr + } + plugins = p + } + + appErr := a.mergePrepackagedPlugins(plugins) + if appErr != nil { + return nil, appErr + } + + appErr = a.mergeLocalPlugins(plugins) + if appErr != nil { + return nil, appErr + } + + // Filter plugins. var result []*model.MarketplacePlugin - pluginSet := map[string]bool{} - pluginsEnvironment := a.GetPluginsEnvironment() - if pluginsEnvironment == nil { - return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError) - } - - marketplaceClient, err := marketplace.NewClient( - *a.Config().PluginSettings.MarketplaceUrl, - a.HTTPService, - ) - if err != nil { - return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - // Fetch all plugins from marketplace. - marketplacePlugins, err := marketplaceClient.GetPlugins(&model.MarketplacePluginFilter{ - PerPage: -1, - ServerVersion: model.CurrentVersion, - }) - if err != nil { - return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.marketplace_plugins.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - for _, p := range marketplacePlugins { - if p.Manifest == nil || !pluginMatchesFilter(p.Manifest, filter.Filter) { - continue + for _, p := range plugins { + if pluginMatchesFilter(p.Manifest, filter.Filter) { + result = append(result, p) } - - marketplacePlugin := &model.MarketplacePlugin{ - BaseMarketplacePlugin: p, - } - - var manifest *model.Manifest - if manifest, err = pluginsEnvironment.GetManifest(p.Manifest.Id); err != nil && err != plugin.ErrNotFound { - return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) - } else if err == nil { - // Plugin is installed. - marketplacePlugin.InstalledVersion = manifest.Version - } - - pluginSet[p.Manifest.Id] = true - result = append(result, marketplacePlugin) - } - - // Include all other installed plugins. - plugins, err := pluginsEnvironment.Available() - if err != nil { - return nil, model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) - } - - for _, plugin := range plugins { - if plugin.Manifest == nil || pluginSet[plugin.Manifest.Id] || !pluginMatchesFilter(plugin.Manifest, filter.Filter) { - continue - } - - result = append(result, &model.MarketplacePlugin{ - BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ - // Labels should not (yet) be localized as the labels sent by the Marketplace are not (yet) localizable. - Labels: []model.MarketplaceLabel{{ - Name: "Local", - Description: "This plugin is not listed in the marketplace but was installed manually", - }}, - Manifest: plugin.Manifest, - }, - InstalledVersion: plugin.Manifest.Version, - }) } // Sort result alphabetically. @@ -521,6 +448,166 @@ func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*m return result, nil } +// getPrepackagedPlugin returns a pre-packaged plugin. +func (a *App) getPrepackagedPlugin(pluginId, version string) (*plugin.PrepackagedPlugin, *model.AppError) { + pluginsEnvironment := a.GetPluginsEnvironment() + if pluginsEnvironment == nil { + return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.config.app_error", nil, "plugin environment is nil", http.StatusInternalServerError) + } + + prepackagedPlugins := pluginsEnvironment.PrepackagedPlugins() + for _, p := range prepackagedPlugins { + if p.Manifest.Id == pluginId && p.Manifest.Version == version { + return p, nil + } + } + + return nil, model.NewAppError("getPrepackagedPlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError) +} + +// getRemoteMarketplacePlugin returns plugin from marketplace-server. +func (a *App) getRemoteMarketplacePlugin(pluginId, version string) (*model.BaseMarketplacePlugin, *model.AppError) { + marketplaceClient, err := marketplace.NewClient( + *a.Config().PluginSettings.MarketplaceUrl, + a.HTTPService, + ) + if err != nil { + return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + filter := &model.MarketplacePluginFilter{Filter: pluginId, ServerVersion: model.CurrentVersion} + plugin, err := marketplaceClient.GetPlugin(filter, version) + if err != nil { + return nil, model.NewAppError("GetMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return plugin, nil +} + +func (a *App) getRemotePlugins(filter *model.MarketplacePluginFilter) (map[string]*model.MarketplacePlugin, *model.AppError) { + result := map[string]*model.MarketplacePlugin{} + + pluginsEnvironment := a.GetPluginsEnvironment() + if pluginsEnvironment == nil { + return nil, model.NewAppError("getRemotePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError) + } + + marketplaceClient, err := marketplace.NewClient( + *a.Config().PluginSettings.MarketplaceUrl, + a.HTTPService, + ) + if err != nil { + return nil, model.NewAppError("getRemotePlugins", "app.plugin.marketplace_client.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + // Fetch all plugins from marketplace. + marketplacePlugins, err := marketplaceClient.GetPlugins(&model.MarketplacePluginFilter{ + PerPage: -1, + ServerVersion: model.CurrentVersion, + }) + if err != nil { + return nil, model.NewAppError("getRemotePlugins", "app.plugin.marketplace_client.failed_to_fetch", nil, err.Error(), http.StatusInternalServerError) + } + + for _, p := range marketplacePlugins { + if p.Manifest == nil { + continue + } + + result[p.Manifest.Id] = &model.MarketplacePlugin{BaseMarketplacePlugin: p} + } + + return result, nil +} + +// mergePrepackagedPlugins merges pre-packaged plugins to remote marketplace plugins list. +func (a *App) mergePrepackagedPlugins(remoteMarketplacePlugins map[string]*model.MarketplacePlugin) *model.AppError { + pluginsEnvironment := a.GetPluginsEnvironment() + if pluginsEnvironment == nil { + return model.NewAppError("mergePrepackagedPlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError) + } + + for _, prepackaged := range pluginsEnvironment.PrepackagedPlugins() { + if prepackaged.Manifest == nil { + continue + } + + prepackagedMarketplace := &model.MarketplacePlugin{ + BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ + Manifest: prepackaged.Manifest, + }, + } + + // If not available in marketplace, add the prepackaged + if remoteMarketplacePlugins[prepackaged.Manifest.Id] == nil { + remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace + continue + } + + // If available in the markteplace, only overwrite if newer. + prepackagedVersion, err := semver.Parse(prepackaged.Manifest.Version) + if err != nil { + return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest) + } + + marketplacePlugin := remoteMarketplacePlugins[prepackaged.Manifest.Id] + marketplaceVersion, err := semver.Parse(marketplacePlugin.Manifest.Version) + if err != nil { + return model.NewAppError("mergePrepackagedPlugins", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest) + } + + if prepackagedVersion.GT(marketplaceVersion) { + remoteMarketplacePlugins[prepackaged.Manifest.Id] = prepackagedMarketplace + } + } + + return nil +} + +// mergeLocalPlugins merges locally installed plugins to remote marketplace plugins list. +func (a *App) mergeLocalPlugins(remoteMarketplacePlugins map[string]*model.MarketplacePlugin) *model.AppError { + pluginsEnvironment := a.GetPluginsEnvironment() + if pluginsEnvironment == nil { + return model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, "", http.StatusInternalServerError) + } + + localPlugins, err := pluginsEnvironment.Available() + if err != nil { + return model.NewAppError("GetMarketplacePlugins", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + for _, plugin := range localPlugins { + if plugin.Manifest == nil { + continue + } + + if remoteMarketplacePlugins[plugin.Manifest.Id] != nil { + // Remote plugin is installed. + remoteMarketplacePlugins[plugin.Manifest.Id].InstalledVersion = plugin.Manifest.Version + continue + } + + var labels []model.MarketplaceLabel + if *a.Config().PluginSettings.EnableRemoteMarketplace { + // Labels should not (yet) be localized as the labels sent by the Marketplace are not (yet) localizable. + labels = append(labels, model.MarketplaceLabel{ + Name: "Local", + Description: "This plugin is not listed in the marketplace", + }) + } + + remoteMarketplacePlugins[plugin.Manifest.Id] = &model.MarketplacePlugin{ + BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ + Labels: labels, + Manifest: plugin.Manifest, + }, + InstalledVersion: plugin.Manifest.Version, + } + } + + return nil +} + func pluginMatchesFilter(manifest *model.Manifest, filter string) bool { filter = strings.TrimSpace(strings.ToLower(filter)) @@ -600,6 +687,10 @@ func (a *App) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.Ap return nil, model.NewAppError("getPluginsFromDir", "app.plugin.sync.list_filestore.app_error", nil, appErr.Error(), http.StatusInternalServerError) } + return getPluginsFromFilePaths(fileStorePaths), nil +} + +func getPluginsFromFilePaths(fileStorePaths []string) map[string]*pluginSignaturePath { pluginSignaturePathMap := make(map[string]*pluginSignaturePath) for _, path := range fileStorePaths { if strings.HasSuffix(path, ".tar.gz") { @@ -623,5 +714,121 @@ func (a *App) getPluginsFromFolder() (map[string]*pluginSignaturePath, *model.Ap } } - return pluginSignaturePathMap, nil + return pluginSignaturePathMap +} + +func (a *App) processPrepackagedPlugins(pluginsDir string) []*plugin.PrepackagedPlugin { + prepackagedPluginsDir, found := fileutils.FindDir(pluginsDir) + if !found { + return nil + } + + fileStorePaths := []string{} + err := filepath.Walk(prepackagedPluginsDir, func(walkPath string, info os.FileInfo, err error) error { + fileStorePaths = append(fileStorePaths, walkPath) + return nil + }) + if err != nil { + mlog.Error("Failed to walk prepackaged plugins", mlog.Err(err)) + return nil + } + + pluginSignaturePathMap := getPluginsFromFilePaths(fileStorePaths) + plugins := make([]*plugin.PrepackagedPlugin, 0, len(pluginSignaturePathMap)) + for _, pluginPaths := range pluginSignaturePathMap { + plugin, err := a.processPrepackagedPlugin(pluginPaths) + if err != nil { + mlog.Error("Failed to install prepackaged plugin", mlog.String("path", pluginPaths.path), mlog.Err(err)) + continue + } + + plugins = append(plugins, plugin) + } + + return plugins +} + +// processPrepackagedPlugin will return the prepackaged plugin metadata and will also +// install the prepackaged plugin if it had been previously enabled and AutomaticPrepackagedPlugins is true. +func (a *App) processPrepackagedPlugin(pluginPath *pluginSignaturePath) (*plugin.PrepackagedPlugin, error) { + mlog.Debug("Processing prepackaged plugin", mlog.String("path", pluginPath.path)) + + fileReader, err := os.Open(pluginPath.path) + if err != nil { + return nil, errors.Wrapf(err, "Failed to open prepackaged plugin %s", pluginPath.path) + } + tmpDir, err := ioutil.TempDir("", "plugintmp") + if err != nil { + return nil, errors.Wrap(err, "Failed to create temp dir plugintmp") + } + defer os.RemoveAll(tmpDir) + + plugin, pluginDir, err := getPrepackagedPlugin(pluginPath, fileReader, tmpDir) + if err != nil { + return nil, errors.Wrapf(err, "Failed to get prepackaged plugin %s", pluginPath.path) + } + + // Skip installing the plugin at all if automatic prepackaged plugins is disabled + if !*a.Config().PluginSettings.AutomaticPrepackagedPlugins { + return plugin, nil + } + + // Skip installing if the plugin is has not been previously enabled. + pluginState := a.Config().PluginSettings.PluginStates[plugin.Manifest.Id] + if pluginState == nil || !pluginState.Enable { + return plugin, nil + } + + mlog.Debug("Installing prepackaged plugin", mlog.String("path", pluginPath.path)) + if _, err := a.installExtractedPlugin(plugin.Manifest, pluginDir, installPluginLocallyOnlyIfNewOrUpgrade); err != nil { + return nil, errors.Wrapf(err, "Failed to install extracted prepackaged plugin %s", pluginPath.path) + } + + return plugin, nil +} + +// getPrepackagedPlugin builds a PrepackagedPlugin from the plugin at the given path, additionally returning the directory in which it was extracted. +func getPrepackagedPlugin(pluginPath *pluginSignaturePath, pluginFile io.ReadSeeker, tmpDir string) (*plugin.PrepackagedPlugin, string, error) { + manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir) + if appErr != nil { + return nil, "", errors.Wrapf(appErr, "Failed to extract plugin with path %s", pluginPath.path) + } + + plugin := new(plugin.PrepackagedPlugin) + plugin.Manifest = manifest + plugin.Path = pluginPath.path + + if pluginPath.signaturePath != "" { + sig := pluginPath.signaturePath + sigReader, sigErr := os.Open(sig) + if sigErr != nil { + return nil, "", errors.Wrapf(sigErr, "Failed to open prepackaged plugin signature %s", sig) + } + bytes, sigErr := ioutil.ReadAll(sigReader) + if sigErr != nil { + return nil, "", errors.Wrapf(sigErr, "Failed to read prepackaged plugin signature %s", sig) + } + plugin.Signature = bytes + } + + if manifest.IconPath != "" { + iconData, err := getIcon(manifest.IconPath) + if err != nil { + return nil, "", errors.Wrapf(err, "Failed to read icon at %s", manifest.IconPath) + } + plugin.IconData = iconData + } + + return plugin, pluginDir, nil +} + +func getIcon(iconPath string) (string, error) { + icon, err := ioutil.ReadFile(iconPath) + if err != nil { + return "", errors.Wrapf(err, "failed to open icon at path %s", iconPath) + } + if !svg.Is(icon) { + return "", errors.Wrapf(err, "icon is not svg %s", iconPath) + } + return fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(icon)), nil } diff --git a/app/plugin_install.go b/app/plugin_install.go index dbf2f2508f..8a55fa538c 100644 --- a/app/plugin_install.go +++ b/app/plugin_install.go @@ -37,6 +37,7 @@ package app import ( + "bytes" "fmt" "io" "io/ioutil" @@ -45,6 +46,8 @@ import ( "path/filepath" "github.com/blang/semver" + "github.com/pkg/errors" + "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/plugin" @@ -169,6 +172,61 @@ func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStr return manifest, nil } +// InstallMarketplacePlugin installs a plugin listed in the marketplace server. It will get the plugin bundle +// from the prepackaged folder, if available, or remotely if EnableRemoteMarketplace is true. +func (a *App) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) { + var pluginFile, signatureFile io.ReadSeeker + + prepackagedPlugin, appErr := a.getPrepackagedPlugin(request.Id, request.Version) + if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" { + return nil, appErr + } + if prepackagedPlugin != nil { + fileReader, err := os.Open(prepackagedPlugin.Path) + if err != nil { + err = errors.Wrapf(err, "failed to open prepackaged plugin %s", prepackagedPlugin.Path) + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, err.Error(), http.StatusInternalServerError) + } + defer fileReader.Close() + + pluginFile = fileReader + signatureFile = bytes.NewReader(prepackagedPlugin.Signature) + } + + if *a.Config().PluginSettings.EnableRemoteMarketplace && pluginFile == nil { + var plugin *model.BaseMarketplacePlugin + plugin, appErr = a.getRemoteMarketplacePlugin(request.Id, request.Version) + if appErr != nil { + return nil, appErr + } + + downloadedPluginBytes, err := a.DownloadFromURL(plugin.DownloadURL) + if err != nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, err.Error(), http.StatusInternalServerError) + } + signature, err := plugin.DecodeSignature() + if err != nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, err.Error(), http.StatusNotImplemented) + } + pluginFile = bytes.NewReader(downloadedPluginBytes) + signatureFile = signature + } + + if pluginFile == nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError) + } + if signatureFile == nil { + return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError) + } + + manifest, appErr := a.InstallPluginWithSignature(pluginFile, signatureFile) + if appErr != nil { + return nil, appErr + } + + return manifest, nil +} + type pluginInstallationStrategy int const ( @@ -185,6 +243,7 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa if pluginsEnvironment == nil { return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } + // verify signature if signature != nil { if err := a.VerifyPlugin(pluginFile, signature); err != nil { @@ -198,33 +257,55 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa } defer os.RemoveAll(tmpDir) - pluginFile.Seek(0, 0) - if err = utils.ExtractTarGz(pluginFile, tmpDir); err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest) + manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir) + if appErr != nil { + return nil, appErr } - tmpPluginDir := tmpDir - dir, err := ioutil.ReadDir(tmpDir) + manifest, appErr = a.installExtractedPlugin(manifest, pluginDir, installationStrategy) + if appErr != nil { + return nil, appErr + } + + return manifest, nil +} + +func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest, string, *model.AppError) { + pluginFile.Seek(0, 0) + if err := utils.ExtractTarGz(pluginFile, extractDir); err != nil { + return nil, "", model.NewAppError("extractPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest) + } + + dir, err := ioutil.ReadDir(extractDir) if err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError) + return nil, "", model.NewAppError("extractPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError) } if len(dir) == 1 && dir[0].IsDir() { - tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name()) + extractDir = filepath.Join(extractDir, dir[0].Name()) } - manifest, _, err := model.FindManifest(tmpPluginDir) + manifest, _, err := model.FindManifest(extractDir) if err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest) + return nil, "", model.NewAppError("extractPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest) } if !plugin.IsValidId(manifest.Id) { - return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidIdRegex}, "", http.StatusBadRequest) + return nil, "", model.NewAppError("extractPlugin", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidIdRegex}, "", http.StatusBadRequest) + } + + return manifest, extractDir, nil +} + +func (a *App) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) { + pluginsEnvironment := a.GetPluginsEnvironment() + if pluginsEnvironment == nil { + return nil, model.NewAppError("installExtractedPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) } bundles, err := pluginsEnvironment.Available() if err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError) } // Check for plugins installed with the same ID. @@ -239,7 +320,7 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa if existingManifest != nil { // Return an error if already installed and strategy disallows installation. if installationStrategy == installPluginLocallyOnlyIfNew { - return nil, model.NewAppError("installPluginLocally", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest) + return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest) } // Skip installation if already installed and newer. @@ -248,12 +329,12 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa version, err = semver.Parse(manifest.Version) if err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest) + return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest) } existingVersion, err = semver.Parse(existingManifest.Version) if err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest) + return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest) } if version.LTE(existingVersion) { @@ -265,37 +346,37 @@ func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installa // Otherwise remove the existing installation prior to install below. mlog.Debug("Removing existing installation of plugin before local install", mlog.String("plugin_id", existingManifest.Id), mlog.String("version", existingManifest.Version)) if err := a.removePluginLocally(existingManifest.Id); err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest) + return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest) } } pluginPath := filepath.Join(*a.Config().PluginSettings.Directory, manifest.Id) - err = utils.CopyDir(tmpPluginDir, pluginPath) + err = utils.CopyDir(fromPluginDir, pluginPath) if err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("installExtractedPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError) } // Flag plugin locally as managed by the filestore. f, err := os.Create(filepath.Join(pluginPath, managedPluginFileName)) if err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.flag_managed.app_error", nil, err.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("installExtractedPlugin", "app.plugin.flag_managed.app_error", nil, err.Error(), http.StatusInternalServerError) } f.Close() if manifest.HasWebapp() { updatedManifest, err := pluginsEnvironment.UnpackWebappBundle(manifest.Id) if err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.webapp_bundle.app_error", nil, err.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("installExtractedPlugin", "app.plugin.webapp_bundle.app_error", nil, err.Error(), http.StatusInternalServerError) } manifest = updatedManifest } - // Activate plugin if it was previously activated. + // Activate the plugin if enabled. pluginState := a.Config().PluginSettings.PluginStates[manifest.Id] if pluginState != nil && pluginState.Enable { updatedManifest, _, err := pluginsEnvironment.Activate(manifest.Id) if err != nil { - return nil, model.NewAppError("installPluginLocally", "app.plugin.restart.app_error", nil, err.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("installExtractedPlugin", "app.plugin.restart.app_error", nil, err.Error(), http.StatusInternalServerError) } manifest = updatedManifest } diff --git a/app/plugin_test.go b/app/plugin_test.go index b209155655..bdc28a6a69 100644 --- a/app/plugin_test.go +++ b/app/plugin_test.go @@ -22,6 +22,7 @@ import ( "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/plugin" + "github.com/mattermost/mattermost-server/v5/utils" "github.com/mattermost/mattermost-server/v5/utils/fileutils" ) @@ -485,108 +486,316 @@ func TestPluginSync(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.Description, func(t *testing.T) { - os.MkdirAll("./test-plugins", os.ModePerm) - defer os.RemoveAll("./test-plugins") - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true - *cfg.PluginSettings.Directory = "./test-plugins" - *cfg.PluginSettings.ClientDirectory = "./test-client-plugins" - *cfg.PluginSettings.RequirePluginSignature = false + testCase.ConfigFunc(cfg) }) - th.App.UpdateConfig(testCase.ConfigFunc) - env, err := plugin.NewEnvironment(th.App.NewPluginAPI, "./test-plugins", "./test-client-plugins", th.App.Log) - require.NoError(t, err) - th.App.SetPluginsEnvironment(env) + env := th.App.GetPluginsEnvironment() + require.NotNil(t, env) - // New bundle in the file store case path, _ := fileutils.FindDir("tests") - fileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz")) - require.NoError(t, err) - defer fileReader.Close() - _, appErr := th.App.WriteFile(fileReader, th.App.getBundleStorePath("testplugin")) - checkNoError(t, appErr) + t.Run("new bundle in the file store", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.RequirePluginSignature = false + }) - appErr = th.App.SyncPlugins() - checkNoError(t, appErr) + fileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz")) + require.NoError(t, err) + defer fileReader.Close() - // Check if installed - pluginStatus, err := env.Statuses() - require.Nil(t, err) - require.Len(t, pluginStatus, 1) - require.Equal(t, pluginStatus[0].PluginId, "testplugin") + _, appErr := th.App.WriteFile(fileReader, th.App.getBundleStorePath("testplugin")) + checkNoError(t, appErr) - // Bundle removed from the file store case - appErr = th.App.RemoveFile(th.App.getBundleStorePath("testplugin")) - checkNoError(t, appErr) + appErr = th.App.SyncPlugins() + checkNoError(t, appErr) - appErr = th.App.SyncPlugins() - checkNoError(t, appErr) - - // Check if removed - pluginStatus, err = env.Statuses() - require.Nil(t, err) - require.Empty(t, pluginStatus) - - // RequirePluginSignature = true case - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.PluginSettings.RequirePluginSignature = true + // Check if installed + pluginStatus, err := env.Statuses() + require.Nil(t, err) + require.Len(t, pluginStatus, 1) + require.Equal(t, pluginStatus[0].PluginId, "testplugin") }) - pluginFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz")) - require.NoError(t, err) - defer pluginFileReader.Close() - _, appErr = th.App.WriteFile(pluginFileReader, th.App.getBundleStorePath("testplugin")) - checkNoError(t, appErr) - // no signature - appErr = th.App.SyncPlugins() - checkNoError(t, appErr) - pluginStatus, err = env.Statuses() - require.Nil(t, err) - require.Empty(t, pluginStatus) - // Wrong signature - signatureFileReader, err := os.Open(filepath.Join(path, "testpluginv2.tar.gz.sig")) - require.NoError(t, err) - defer signatureFileReader.Close() - filePath := fmt.Sprintf("%s.sig", th.App.getBundleStorePath("testplugin")) - _, appErr = th.App.WriteFile(signatureFileReader, filePath) - checkNoError(t, appErr) + t.Run("bundle removed from the file store", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.RequirePluginSignature = false + }) - appErr = th.App.SyncPlugins() - checkNoError(t, appErr) + appErr := th.App.RemoveFile(th.App.getBundleStorePath("testplugin")) + checkNoError(t, appErr) - pluginStatus, err = env.Statuses() - require.Nil(t, err) - require.Empty(t, pluginStatus) + appErr = th.App.SyncPlugins() + checkNoError(t, appErr) - // Correct signature - key, err := os.Open(filepath.Join(path, "development-private-key.asc")) - require.NoError(t, err) - appErr = th.App.AddPublicKey("pub_key", key) - checkNoError(t, appErr) + // Check if removed + pluginStatus, err := env.Statuses() + require.Nil(t, err) + require.Empty(t, pluginStatus) + }) - signatureFileReader, err = os.Open(filepath.Join(path, "testplugin.tar.gz.sig")) - require.NoError(t, err) - defer signatureFileReader.Close() - filePath = fmt.Sprintf("%s.sig", th.App.getBundleStorePath("testplugin")) - _, appErr = th.App.WriteFile(signatureFileReader, filePath) - checkNoError(t, appErr) + t.Run("plugin signatures required, no signature", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.RequirePluginSignature = true + }) - appErr = th.App.SyncPlugins() - checkNoError(t, appErr) + pluginFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz")) + require.NoError(t, err) + defer pluginFileReader.Close() + _, appErr := th.App.WriteFile(pluginFileReader, th.App.getBundleStorePath("testplugin")) + checkNoError(t, appErr) - pluginStatus, err = env.Statuses() - require.Nil(t, err) - require.Len(t, pluginStatus, 1) - require.Equal(t, pluginStatus[0].PluginId, "testplugin") + appErr = th.App.SyncPlugins() + checkNoError(t, appErr) + pluginStatus, err := env.Statuses() + require.Nil(t, err) + require.Len(t, pluginStatus, 0) + }) - appErr = th.App.DeletePublicKey("pub_key") - checkNoError(t, appErr) + t.Run("plugin signatures required, wrong signature", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.RequirePluginSignature = true + }) - appErr = th.App.RemovePlugin("testplugin") - checkNoError(t, appErr) + signatureFileReader, err := os.Open(filepath.Join(path, "testplugin2.tar.gz.sig")) + require.NoError(t, err) + defer signatureFileReader.Close() + _, appErr := th.App.WriteFile(signatureFileReader, th.App.getSignatureStorePath("testplugin")) + checkNoError(t, appErr) + + appErr = th.App.SyncPlugins() + checkNoError(t, appErr) + + pluginStatus, err := env.Statuses() + require.Nil(t, err) + require.Len(t, pluginStatus, 0) + }) + + t.Run("plugin signatures required, correct signature", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.RequirePluginSignature = true + }) + + key, err := os.Open(filepath.Join(path, "development-private-key.asc")) + require.NoError(t, err) + appErr := th.App.AddPublicKey("pub_key", key) + checkNoError(t, appErr) + + signatureFileReader, err := os.Open(filepath.Join(path, "testplugin.tar.gz.sig")) + require.NoError(t, err) + defer signatureFileReader.Close() + _, appErr = th.App.WriteFile(signatureFileReader, th.App.getSignatureStorePath("testplugin")) + checkNoError(t, appErr) + + appErr = th.App.SyncPlugins() + checkNoError(t, appErr) + + pluginStatus, err := env.Statuses() + require.Nil(t, err) + require.Len(t, pluginStatus, 1) + require.Equal(t, pluginStatus[0].PluginId, "testplugin") + + appErr = th.App.DeletePublicKey("pub_key") + checkNoError(t, appErr) + + appErr = th.App.RemovePlugin("testplugin") + checkNoError(t, appErr) + }) }) } } + +func TestProcessPrepackagedPlugins(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + testsPath, _ := fileutils.FindDir("tests") + prepackagedPluginsPath := filepath.Join(testsPath, prepackagedPluginsDir) + fileErr := os.Mkdir(prepackagedPluginsPath, os.ModePerm) + require.NoError(t, fileErr) + defer os.RemoveAll(prepackagedPluginsPath) + + prepackagedPluginsDir, found := fileutils.FindDir(prepackagedPluginsPath) + require.True(t, found, "failed to find prepackaged plugins directory") + + testPluginPath := filepath.Join(testsPath, "testplugin.tar.gz") + fileErr = utils.CopyFile(testPluginPath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz")) + require.NoError(t, fileErr) + + t.Run("automatic, enabled plugin, no signature", func(t *testing.T) { + // Install the plugin and enable + pluginBytes, err := ioutil.ReadFile(testPluginPath) + require.NoError(t, err) + require.NotNil(t, pluginBytes) + + manifest, appErr := th.App.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) + require.Nil(t, appErr) + require.Equal(t, "testplugin", manifest.Id) + + env := th.App.GetPluginsEnvironment() + + activatedManifest, activated, err := env.Activate(manifest.Id) + require.NoError(t, err) + require.True(t, activated) + require.Equal(t, manifest, activatedManifest) + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + }) + + plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir) + require.Len(t, plugins, 1) + require.Equal(t, plugins[0].Manifest.Id, "testplugin") + require.Empty(t, plugins[0].Signature, 0) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 1) + require.Equal(t, pluginStatus[0].PluginId, "testplugin") + + appErr = th.App.RemovePlugin("testplugin") + checkNoError(t, appErr) + + pluginStatus, err = env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 0) + }) + + t.Run("automatic, not enabled plugin", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + }) + + env := th.App.GetPluginsEnvironment() + + plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir) + require.Len(t, plugins, 1) + require.Equal(t, plugins[0].Manifest.Id, "testplugin") + require.Empty(t, plugins[0].Signature, 0) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + require.Empty(t, pluginStatus, 0) + }) + + t.Run("automatic, multiple plugins with signatures, not enabled", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + }) + + env := th.App.GetPluginsEnvironment() + + // Add signature + testPluginSignaturePath := filepath.Join(testsPath, "testplugin.tar.gz.sig") + err := utils.CopyFile(testPluginSignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig")) + require.NoError(t, err) + + // Add second plugin + testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz") + err = utils.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz")) + require.NoError(t, err) + + testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig") + err = utils.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) + require.NoError(t, err) + + plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir) + require.Len(t, plugins, 2) + require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) + require.NotEmpty(t, plugins[0].Signature) + require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id) + require.NotEmpty(t, plugins[1].Signature) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 0) + }) + + t.Run("automatic, multiple plugins with signatures, one enabled", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.AutomaticPrepackagedPlugins = true + }) + + env := th.App.GetPluginsEnvironment() + + // Add signature + testPluginSignaturePath := filepath.Join(testsPath, "testplugin.tar.gz.sig") + err := utils.CopyFile(testPluginSignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin.tar.gz.sig")) + require.NoError(t, err) + + // Install first plugin and enable + pluginBytes, err := ioutil.ReadFile(testPluginPath) + require.NoError(t, err) + require.NotNil(t, pluginBytes) + + manifest, appErr := th.App.installPluginLocally(bytes.NewReader(pluginBytes), nil, installPluginLocallyAlways) + require.Nil(t, appErr) + require.Equal(t, "testplugin", manifest.Id) + + activatedManifest, activated, err := env.Activate(manifest.Id) + require.NoError(t, err) + require.True(t, activated) + require.Equal(t, manifest, activatedManifest) + + // Add second plugin + testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz") + err = utils.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz")) + require.NoError(t, err) + + testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig") + err = utils.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) + require.NoError(t, err) + + plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir) + require.Len(t, plugins, 2) + require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) + require.NotEmpty(t, plugins[0].Signature) + require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id) + require.NotEmpty(t, plugins[1].Signature) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 1) + require.Equal(t, pluginStatus[0].PluginId, "testplugin") + + appErr = th.App.RemovePlugin("testplugin") + checkNoError(t, appErr) + + pluginStatus, err = env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 0) + }) + + t.Run("non-automatic, multiple plugins", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.AutomaticPrepackagedPlugins = false + }) + + env := th.App.GetPluginsEnvironment() + + testPlugin2Path := filepath.Join(testsPath, "testplugin2.tar.gz") + err := utils.CopyFile(testPlugin2Path, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz")) + require.NoError(t, err) + + testPlugin2SignaturePath := filepath.Join(testsPath, "testplugin2.tar.gz.sig") + err = utils.CopyFile(testPlugin2SignaturePath, filepath.Join(prepackagedPluginsDir, "testplugin2.tar.gz.sig")) + require.NoError(t, err) + + plugins := th.App.processPrepackagedPlugins(prepackagedPluginsDir) + require.Len(t, plugins, 2) + require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[0].Manifest.Id) + require.NotEmpty(t, plugins[0].Signature) + require.Contains(t, []string{"testplugin", "testplugin2"}, plugins[1].Manifest.Id) + require.NotEmpty(t, plugins[1].Signature) + + pluginStatus, err := env.Statuses() + require.NoError(t, err) + require.Len(t, pluginStatus, 0) + }) +} diff --git a/build/release.mk b/build/release.mk index 2c51900ad8..aca6cfb228 100644 --- a/build/release.mk +++ b/build/release.mk @@ -67,6 +67,7 @@ endif mkdir -p tmpprepackaged @cd tmpprepackaged && for plugin_package in $(PLUGIN_PACKAGES) ; do \ curl -O -L https://plugins-store.test.mattermost.com/release/$$plugin_package.tar.gz; \ + curl -O -L https://plugins-store.test.mattermost.com/release/$$plugin_package.tar.gz.sig; \ done @# ----- PLATFORM SPECIFIC ----- @@ -83,6 +84,7 @@ endif @# Strip and prepackage plugins @for plugin_package in $(PLUGIN_PACKAGES) ; do \ cat tmpprepackaged/$$plugin_package.tar.gz | gunzip | tar --wildcards --delete "*windows*" --delete "*linux*" | gzip > $(DIST_PATH)/prepackaged_plugins/$$plugin_package.tar.gz; \ + cp tmpprepackaged/$$plugin_package.tar.gz.sig $(DIST_PATH)/prepackaged_plugins; \ done @# Package tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-osx-amd64.tar.gz mattermost @@ -103,6 +105,7 @@ endif @# Strip and prepackage plugins @for plugin_package in $(PLUGIN_PACKAGES) ; do \ cat tmpprepackaged/$$plugin_package.tar.gz | gunzip | tar --wildcards --delete "*darwin*" --delete "*linux*" | gzip > $(DIST_PATH)/prepackaged_plugins/$$plugin_package.tar.gz; \ + cp tmpprepackaged/$$plugin_package.tar.gz.sig $(DIST_PATH)/prepackaged_plugins; \ done @# Package cd $(DIST_ROOT) && zip -9 -r -q -l mattermost-$(BUILD_TYPE_NAME)-windows-amd64.zip mattermost && cd .. @@ -123,6 +126,7 @@ endif @# Strip and prepackage plugins @for plugin_package in $(PLUGIN_PACKAGES) ; do \ cat tmpprepackaged/$$plugin_package.tar.gz | gunzip | tar --wildcards --delete "*windows*" --delete "*darwin*" | gzip > $(DIST_PATH)/prepackaged_plugins/$$plugin_package.tar.gz; \ + cp tmpprepackaged/$$plugin_package.tar.gz.sig $(DIST_PATH)/prepackaged_plugins; \ done @# Package tar -C dist -czf $(DIST_PATH)-$(BUILD_TYPE_NAME)-linux-amd64.tar.gz mattermost diff --git a/go.mod b/go.mod index 5191fedecd..c1d34bdc68 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/gorilla/schema v1.1.0 github.com/gorilla/websocket v1.4.1 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84 github.com/hashicorp/go-hclog v0.9.2 github.com/hashicorp/go-immutable-radix v1.1.0 // indirect diff --git a/go.sum b/go.sum index a7cf20cc17..aebdd5d0ae 100644 --- a/go.sum +++ b/go.sum @@ -155,6 +155,8 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:Fecb github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c h1:fEE5/5VNnYUoBOj2I9TP8Jc+a7lge3QWn9DKE7NCwfc= +github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c/go.mod h1:ObS/W+h8RYb1Y7fYivughjxojTmIu5iAIjSrSLCLeqE= github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84 h1:RvcDqcKLua4b/jtXez7ZVe9s6Iq5N6ujVevqY4FBQmM= github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= diff --git a/i18n/en.json b/i18n/en.json index 0c5e715198..7a10d0f68c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1572,18 +1572,6 @@ "id": "api.plugin.install.download_failed.app_error", "translation": "An error occurred while downloading the plugin." }, - { - "id": "api.plugin.install.insecure_url.app_error", - "translation": "An insecure url was given to download the plugin. Please provide a secure url or enable PluginSettings.AllowInsecureDownloadUrl in your configuration." - }, - { - "id": "api.plugin.install.invalid_url.app_error", - "translation": "An invalid url was given to download the plugin." - }, - { - "id": "api.plugin.install.reading_stream_failed.app_error", - "translation": "An error ocurred reading the plugin file stream." - }, { "id": "api.plugin.upload.array.app_error", "translation": "File array is empty in multipart/form request" @@ -3584,7 +3572,7 @@ }, { "id": "app.plugin.extract.app_error", - "translation": "Encountered error extracting plugin" + "translation": "An error occurred extracting the plugin bundle." }, { "id": "app.plugin.filesystem.app_error", @@ -3622,6 +3610,10 @@ "id": "app.plugin.install_id_failed_remove.app_error", "translation": "Unable to install plugin. A plugin with the same ID is already installed and failed to be removed." }, + { + "id": "app.plugin.install_marketplace_plugin.app_error", + "translation": "Failed to install marketplace plugin." + }, { "id": "app.plugin.invalid_id.app_error", "translation": "Plugin Id must be at least {{.Min}} characters, at most {{.Max}} characters and match {{.Regex}}." @@ -3638,6 +3630,10 @@ "id": "app.plugin.marketplace_client.app_error", "translation": "Failed to create marketplace client." }, + { + "id": "app.plugin.marketplace_client.failed_to_fetch", + "translation": "Failed to get plugins from the marketplace server." + }, { "id": "app.plugin.marketplace_disabled.app_error", "translation": "Marketplace has been disabled. Please check your logs for details." @@ -3646,14 +3642,14 @@ "id": "app.plugin.marketplace_plugin_request.app_error", "translation": "Failed to decode the marketplace plugin request." }, - { - "id": "app.plugin.marketplace_plugins.app_error", - "translation": "Failed to get plugins from the marketplace server." - }, { "id": "app.plugin.marketplace_plugins.not_found.app_error", "translation": "Could not find the requested marketplace plugin." }, + { + "id": "app.plugin.marketplace_plugins.signature_not_found.app_error", + "translation": "Could not find the requested marketplace plugin signature." + }, { "id": "app.plugin.marshal.app_error", "translation": "Failed to marshal marketplace plugins." diff --git a/model/config.go b/model/config.go index 083c0e3cf8..5cd61cb82e 100644 --- a/model/config.go +++ b/model/config.go @@ -2265,18 +2265,20 @@ type PluginState struct { } type PluginSettings struct { - Enable *bool - EnableUploads *bool `restricted:"true"` - AllowInsecureDownloadUrl *bool `restricted:"true"` - EnableHealthCheck *bool `restricted:"true"` - Directory *string `restricted:"true"` - ClientDirectory *string `restricted:"true"` - Plugins map[string]map[string]interface{} - PluginStates map[string]*PluginState - EnableMarketplace *bool - RequirePluginSignature *bool - MarketplaceUrl *string - SignaturePublicKeyFiles []string + Enable *bool + EnableUploads *bool `restricted:"true"` + AllowInsecureDownloadUrl *bool `restricted:"true"` + EnableHealthCheck *bool `restricted:"true"` + Directory *string `restricted:"true"` + ClientDirectory *string `restricted:"true"` + Plugins map[string]map[string]interface{} + PluginStates map[string]*PluginState + EnableMarketplace *bool + EnableRemoteMarketplace *bool + AutomaticPrepackagedPlugins *bool + RequirePluginSignature *bool + MarketplaceUrl *string + SignaturePublicKeyFiles []string } func (s *PluginSettings) SetDefaults(ls LogSettings) { @@ -2321,6 +2323,14 @@ func (s *PluginSettings) SetDefaults(ls LogSettings) { s.EnableMarketplace = NewBool(PLUGIN_SETTINGS_DEFAULT_ENABLE_MARKETPLACE) } + if s.EnableRemoteMarketplace == nil { + s.EnableRemoteMarketplace = NewBool(true) + } + + if s.AutomaticPrepackagedPlugins == nil { + s.AutomaticPrepackagedPlugins = NewBool(true) + } + if s.MarketplaceUrl == nil || *s.MarketplaceUrl == "" || *s.MarketplaceUrl == PLUGIN_SETTINGS_OLD_MARKETPLACE_URL { s.MarketplaceUrl = NewString(PLUGIN_SETTINGS_DEFAULT_MARKETPLACE_URL) } diff --git a/model/marketplace_plugin.go b/model/marketplace_plugin.go index b7e55d83c6..0e999bc244 100644 --- a/model/marketplace_plugin.go +++ b/model/marketplace_plugin.go @@ -78,6 +78,7 @@ type MarketplacePluginFilter struct { PerPage int Filter string ServerVersion string + LocalOnly bool } // ApplyToURL modifies the given url to include query string parameters for the request. @@ -89,6 +90,7 @@ func (filter *MarketplacePluginFilter) ApplyToURL(u *url.URL) { } q.Add("filter", filter.Filter) q.Add("server_version", filter.ServerVersion) + q.Add("local_only", strconv.FormatBool(filter.LocalOnly)) u.RawQuery = q.Encode() } diff --git a/plugin/environment.go b/plugin/environment.go index 38b0a6dd2a..5114474fc8 100644 --- a/plugin/environment.go +++ b/plugin/environment.go @@ -36,17 +36,27 @@ type registeredPlugin struct { supervisor *supervisor } +// PrepackagedPlugin is a plugin prepackaged with the server and found on startup. +type PrepackagedPlugin struct { + Path string + IconData string + Manifest *model.Manifest + Signature []byte +} + // Environment represents the execution environment of active plugins. // // It is meant for use by the Mattermost server to manipulate, interact with and report on the set // of active plugins. type Environment struct { - registeredPlugins sync.Map - pluginHealthCheckJob *PluginHealthCheckJob - logger *mlog.Logger - newAPIImpl apiImplCreatorFunc - pluginDir string - webappPluginDir string + registeredPlugins sync.Map + pluginHealthCheckJob *PluginHealthCheckJob + logger *mlog.Logger + newAPIImpl apiImplCreatorFunc + pluginDir string + webappPluginDir string + prepackagedPlugins []*PrepackagedPlugin + prepackagedPluginsLock sync.RWMutex } func NewEnvironment(newAPIImpl apiImplCreatorFunc, pluginDir string, webappPluginDir string, logger *mlog.Logger) (*Environment, error) { @@ -87,6 +97,15 @@ func (env *Environment) Available() ([]*model.BundleInfo, error) { return scanSearchPath(env.pluginDir) } +// Returns a list of prepackaged plugins available in the local prepackaged_plugins folder. +// The list content is immutable and should not be modified. +func (env *Environment) PrepackagedPlugins() []*PrepackagedPlugin { + env.prepackagedPluginsLock.RLock() + defer env.prepackagedPluginsLock.RUnlock() + + return env.prepackagedPlugins +} + // Returns a list of all currently active plugins within the environment. func (env *Environment) Active() []*model.BundleInfo { activePlugins := []*model.BundleInfo{} @@ -439,6 +458,13 @@ func (env *Environment) RunMultiPluginHook(hookRunnerFunc func(hooks Hooks) bool }) } +// SetPrepackagedPlugins saves prepackaged plugins in the environment. +func (env *Environment) SetPrepackagedPlugins(plugins []*PrepackagedPlugin) { + env.prepackagedPluginsLock.Lock() + env.prepackagedPlugins = plugins + env.prepackagedPluginsLock.Unlock() +} + func newRegisteredPlugin(bundle *model.BundleInfo) *registeredPlugin { state := model.PluginStateNotRunning return ®isteredPlugin{failTimeStamps: []time.Time{}, State: state, BundleInfo: bundle} diff --git a/tests/testplugin.tar.gz b/tests/testplugin.tar.gz index 4b9b244d03..2dbbcc2116 100644 Binary files a/tests/testplugin.tar.gz and b/tests/testplugin.tar.gz differ diff --git a/tests/testplugin.tar.gz.asc b/tests/testplugin.tar.gz.asc index f03cec92b1..c35e10f169 100644 --- a/tests/testplugin.tar.gz.asc +++ b/tests/testplugin.tar.gz.asc @@ -1,14 +1,15 @@ ------BEGIN PGP SIGNATURE----- +-----BEGIN PGP ARMORED FILE----- +Comment: Use "gpg --dearmor" for unpacking -iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAl26+QIACgkQTHxlYsGS -zB9SFwwAmexJelfpTRjnABJKWnFKuGOTNosVNOWqE+k//n1PV3H3uCzsluh/GWIb -p5Nk6+UJieNvyAaQhIT5+3Rv0HkYvz4N15813SvQ8KOL8vE7oRFHttsZHpDKTpk1 -fQrd9JISMdViVj4x8VltgYkUIJDmULPfF9/m1OGV/BsWuHmmaEp3yw+KLdhL1j3U -LlfW453nHvXrGT2rO3l+KDIVMt4zG6LVmL1C29yWtBplKxjgXKg/EbPcI31/8D9X -QoIz9fUIFzqt4L1IfJhL6NlUiDc1iSKf4STiU2CzmHdKqRD9FdwgzZlG5P/xxCgu -BVNy/c9musZbK9LtapncC/S62uwZzH5naTrCDs4F1VE2P5gjU9iv3AXVaeT4hLuI -EbluXzmAt9hjox2s0O0X6EgW68FbUVX9bvu1tY9sUvWlXNCL2hrybF3YS3cZ2wGx -rmdWmwktXDpJ8iN/b1+quw0CVC7myAefPK0U8356tktZHwcQEp2fmrF4PqX2qfJf -cDzhj2yg -=J9kV ------END PGP SIGNATURE----- +iQGzBAABCAAdFiEE8/rOReDeZCyL1qjmTHxlYsGSzB8FAl3vHFQACgkQTHxlYsGS +zB933gv5AcDL/MW5/fwsLnhWW9plSCbRTMOckt94r0ASOKRzPP7ngEzGtkCFEIGQ +bN/T60s9uADuIRHEFhmtZvG2MOg9Z688BxiDpyyTx7ENZH0zzPnULEg5hqf7DJqK +Q8IdPSNE4wk4gvJLdrYIYUkQT4TkQonmBbIHvj3wWKaXTheDnUNp90pOiTszKubK +ghpAj6ZAlRK60HuMMF7v0RIhqLKYKUGUWvzClKEUZlAKlqTK++FTM0Zot/7KTLDz +KEv8gf6xe4oL7vSQBoZDa3Vvcn6tPF0gvwg2DrITT3MG2Rmgpbc74iUXWqoZ+XQp +zf1P+vxbfbyKMYZ4tERlKfwnl6Dbz5vBvlz/U+ZYBmwuw4wD2DvdrecPHj1KcBNy +2Auap8qhMQcZvYV7D/qnYL8/QU4roQdg4z+G/S0vQTlK5izpzY+/M8ofZHtjDdWD +cD3w1cvwBFKn9EBJ1HA4/7FEHUzFrh/kWRw2PpUjkydCur2rHqecpRXBqS+Haz7J +OT9k54ab +=ywEK +-----END PGP ARMORED FILE----- diff --git a/tests/testplugin.tar.gz.sig b/tests/testplugin.tar.gz.sig index e53a7f53a9..6591ada49c 100644 Binary files a/tests/testplugin.tar.gz.sig and b/tests/testplugin.tar.gz.sig differ diff --git a/tests/testplugin2.tar.gz b/tests/testplugin2.tar.gz new file mode 100644 index 0000000000..635d01dafd Binary files /dev/null and b/tests/testplugin2.tar.gz differ diff --git a/tests/testplugin2.tar.gz.sig b/tests/testplugin2.tar.gz.sig new file mode 100644 index 0000000000..f530bff762 Binary files /dev/null and b/tests/testplugin2.tar.gz.sig differ diff --git a/tests/testpluginv2.tar.gz b/tests/testpluginv2.tar.gz deleted file mode 100644 index 4329a25669..0000000000 Binary files a/tests/testpluginv2.tar.gz and /dev/null differ diff --git a/tests/testpluginv2.tar.gz.sig b/tests/testpluginv2.tar.gz.sig deleted file mode 100644 index edfb7d8496..0000000000 Binary files a/tests/testpluginv2.tar.gz.sig and /dev/null differ diff --git a/vendor/github.com/h2non/go-is-svg/.editorconfig b/vendor/github.com/h2non/go-is-svg/.editorconfig new file mode 100644 index 0000000000..000dc0a7aa --- /dev/null +++ b/vendor/github.com/h2non/go-is-svg/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tabs +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/vendor/github.com/h2non/go-is-svg/.gitignore b/vendor/github.com/h2non/go-is-svg/.gitignore new file mode 100644 index 0000000000..3cf2565219 --- /dev/null +++ b/vendor/github.com/h2non/go-is-svg/.gitignore @@ -0,0 +1,7 @@ +/bimg +/bundle +bin +/*.jpg +/*.png +/*.webp +/fixtures/*_out.* diff --git a/vendor/github.com/h2non/go-is-svg/.travis.yml b/vendor/github.com/h2non/go-is-svg/.travis.yml new file mode 100644 index 0000000000..d5a81534a0 --- /dev/null +++ b/vendor/github.com/h2non/go-is-svg/.travis.yml @@ -0,0 +1,23 @@ +language: go + +go: + - 1.5 + - 1.6 + - 1.7 + - tip + +before_install: + - go get github.com/nbio/st + - go get -u -v github.com/axw/gocov/gocov + - go get -u -v github.com/mattn/goveralls + - go get -u -v github.com/golang/lint/golint + +script: + - diff -u <(echo -n) <(gofmt -s -d ./) + - diff -u <(echo -n) <(go vet ./...) + - diff -u <(echo -n) <(golint ./...) + - go test -v -race ./... + - go test -v -race -covermode=atomic -coverprofile=coverage.out + +after_success: + - goveralls -coverprofile=coverage.out -service=travis-ci diff --git a/vendor/github.com/h2non/go-is-svg/LICENSE b/vendor/github.com/h2non/go-is-svg/LICENSE new file mode 100644 index 0000000000..f67807d007 --- /dev/null +++ b/vendor/github.com/h2non/go-is-svg/LICENSE @@ -0,0 +1,24 @@ +The MIT License + +Copyright (c) 2016 Tomas Aparicio + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/h2non/go-is-svg/README.md b/vendor/github.com/h2non/go-is-svg/README.md new file mode 100644 index 0000000000..d4a0ee6c02 --- /dev/null +++ b/vendor/github.com/h2non/go-is-svg/README.md @@ -0,0 +1,47 @@ +# go-is-svg [![Build Status](https://travis-ci.org/h2non/go-is-svg.png)](https://travis-ci.org/h2non/go-is-svg) [![GoDoc](https://godoc.org/github.com/h2non/go-is-svg?status.svg)](https://godoc.org/github.com/h2non/go-is-svg) [![Coverage Status](https://coveralls.io/repos/github/h2non/go-is-svg/badge.svg?branch=master)](https://coveralls.io/github/h2non/go-is-svg?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/h2non/go-is-svg)](https://goreportcard.com/report/github.com/h2non/go-is-svg) + +Tiny package to verify if a given file buffer is an SVG image in Go (golang). + +See also [filetype](https://github.com/h2non/filetype) package for binary files type inference. + +## Installation + +```bash +go get -u github.com/h2non/go-is-svg +``` + +## Example + +```go +package main + +import ( + "fmt" + "io/ioutil" + + svg "github.com/h2non/go-is-svg" +) + +func main() { + buf, err := ioutil.ReadFile("_example/example.svg") + if err != nil { + fmt.Printf("Error: %s\n", err) + return + } + + if svg.Is(buf) { + fmt.Println("File is an SVG") + } else { + fmt.Println("File is NOT an SVG") + } +} +``` + +Run example: +```bash +go run _example/example.go +``` + +## License + +MIT - Tomas Aparicio diff --git a/vendor/github.com/h2non/go-is-svg/svg.go b/vendor/github.com/h2non/go-is-svg/svg.go new file mode 100644 index 0000000000..062f6e1f66 --- /dev/null +++ b/vendor/github.com/h2non/go-is-svg/svg.go @@ -0,0 +1,36 @@ +package issvg + +import ( + "regexp" + "unicode/utf8" +) + +var ( + htmlCommentRegex = regexp.MustCompile("(?i)") + svgRegex = regexp.MustCompile(`(?i)^\s*(?:<\?xml[^>]*>\s*)?(?:]*>\s*)?]*>[^*]*<\/svg>\s*$`) +) + +// isBinary checks if the given buffer is a binary file. +func isBinary(buf []byte) bool { + if len(buf) < 24 { + return false + } + for i := 0; i < 24; i++ { + charCode, _ := utf8.DecodeRuneInString(string(buf[i])) + if charCode == 65533 || charCode <= 8 { + return true + } + } + return false +} + +// Is returns true if the given buffer is a valid SVG image. +func Is(buf []byte) bool { + return !isBinary(buf) && svgRegex.Match(htmlCommentRegex.ReplaceAll(buf, []byte{})) +} + +// IsSVG returns true if the given buffer is a valid SVG image. +// Alias to: Is() +func IsSVG(buf []byte) bool { + return Is(buf) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 7a39eb1b37..bbb50ba690 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -52,6 +52,8 @@ github.com/gorilla/schema github.com/gorilla/websocket # github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 github.com/gregjones/httpcache +# github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c +github.com/h2non/go-is-svg # github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84 github.com/hako/durafmt # github.com/hashicorp/errwrap v1.0.0