diff --git a/api4/plugin.go b/api4/plugin.go index 519013f2f1..0c2b576a26 100644 --- a/api4/plugin.go +++ b/api4/plugin.go @@ -7,6 +7,7 @@ package api4 import ( "net/http" + "net/url" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" @@ -22,6 +23,7 @@ func (api *API) InitPlugin() { api.BaseRoutes.Plugins.Handle("", api.ApiSessionRequired(uploadPlugin)).Methods("POST") api.BaseRoutes.Plugins.Handle("", api.ApiSessionRequired(getPlugins)).Methods("GET") api.BaseRoutes.Plugin.Handle("", api.ApiSessionRequired(removePlugin)).Methods("DELETE") + api.BaseRoutes.Plugins.Handle("/install_from_url", api.ApiSessionRequired(installPluginFromUrl)).Methods("POST") api.BaseRoutes.Plugins.Handle("/statuses", api.ApiSessionRequired(getPluginStatuses)).Methods("GET") api.BaseRoutes.Plugin.Handle("/enable", api.ApiSessionRequired(enablePlugin)).Methods("POST") @@ -81,6 +83,58 @@ func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(manifest.ToJson())) } +func installPluginFromUrl(c *Context, w http.ResponseWriter, r *http.Request) { + if !*c.App.Config().PluginSettings.Enable { + c.Err = model.NewAppError("installPluginFromUrl", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + return + } + + if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + downloadUrl := r.URL.Query().Get("plugin_download_url") + + if !model.IsValidHttpUrl(downloadUrl) { + c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.invalid_url.app_error", nil, "", http.StatusBadRequest) + return + } + + u, err := url.ParseRequestURI(downloadUrl) + if err != nil { + c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.invalid_url.app_error", nil, "", http.StatusBadRequest) + return + } + + if !*c.App.Config().PluginSettings.AllowInsecureDownloadUrl && u.Scheme != "https" { + c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.insecure_url.app_error", nil, "", http.StatusBadRequest) + return + } + + client := c.App.HTTPService.MakeClient(true) + resp, err := client.Get(downloadUrl) + if err != nil { + c.Err = model.NewAppError("installPluginFromUrl", "api.plugin.install.download_failed.app_error", nil, err.Error(), http.StatusBadRequest) + return + } + defer resp.Body.Close() + + force := false + if r.URL.Query().Get("force") == "true" { + force = true + } + manifest, unpackErr := c.App.InstallPlugin(resp.Body, force) + + if unpackErr != nil { + c.Err = unpackErr + return + } + + w.WriteHeader(http.StatusCreated) + w.Write([]byte(manifest.ToJson())) +} + func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) { if !*c.App.Config().PluginSettings.Enable { c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) diff --git a/api4/plugin_test.go b/api4/plugin_test.go index e17d96ecea..9b24c579f7 100644 --- a/api4/plugin_test.go +++ b/api4/plugin_test.go @@ -7,6 +7,8 @@ import ( "bytes" "encoding/json" "io/ioutil" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" @@ -26,6 +28,7 @@ func TestPlugin(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true *cfg.PluginSettings.EnableUploads = true + *cfg.PluginSettings.AllowInsecureDownloadUrl = true }) path, _ := fileutils.FindDir("tests") @@ -34,9 +37,52 @@ func TestPlugin(t *testing.T) { t.Fatal(err) } - // Successful upload - manifest, resp := th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) + // Install from URL + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + res.Write(tarData) + })) + defer func() { testServer.Close() }() + + url := testServer.URL + + manifest, resp := th.SystemAdminClient.InstallPluginFromUrl(url, false) CheckNoError(t, resp) + assert.Equal(t, "testplugin", manifest.Id) + + _, resp = th.SystemAdminClient.InstallPluginFromUrl(url, false) + CheckBadRequestStatus(t, resp) + + manifest, resp = th.SystemAdminClient.InstallPluginFromUrl(url, true) + CheckNoError(t, resp) + assert.Equal(t, "testplugin", manifest.Id) + + th.App.RemovePlugin(manifest.Id) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = false }) + + _, resp = th.SystemAdminClient.InstallPluginFromUrl(url, false) + CheckNotImplementedStatus(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.Enable = true }) + + _, resp = th.Client.InstallPluginFromUrl(url, false) + CheckForbiddenStatus(t, resp) + + _, resp = th.SystemAdminClient.InstallPluginFromUrl("http://nodata", false) + CheckBadRequestStatus(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.AllowInsecureDownloadUrl = false }) + + _, resp = th.SystemAdminClient.InstallPluginFromUrl(url, false) + CheckBadRequestStatus(t, resp) + + // Successful upload + manifest, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader(tarData)) + CheckNoError(t, resp) + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.EnableUploads = true }) + manifest, resp = th.SystemAdminClient.UploadPluginForced(bytes.NewReader(tarData)) defer os.RemoveAll("plugins/testplugin") CheckNoError(t, resp) diff --git a/app/diagnostics.go b/app/diagnostics.go index d7957c4e19..ddf7f456a7 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -581,6 +581,7 @@ func (a *App) trackConfig() { "enable_zoom": pluginActivated(cfg.PluginSettings.PluginStates, "zoom"), "enable": *cfg.PluginSettings.Enable, "enable_uploads": *cfg.PluginSettings.EnableUploads, + "allow_insecure_download_url": *cfg.PluginSettings.AllowInsecureDownloadUrl, }) a.SendDiagnostic(TRACK_CONFIG_DATA_RETENTION, map[string]interface{}{ diff --git a/i18n/en.json b/i18n/en.json index dd068ffb3b..9a521b9394 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1464,6 +1464,18 @@ "id": "api.outgoing_webhook.disabled.app_error", "translation": "Outgoing webhooks have been disabled by the system admin." }, + { + "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.upload.array.app_error", "translation": "File array is empty in multipart/form request" diff --git a/model/client4.go b/model/client4.go index 7bcefc8b30..3eaaa1e198 100644 --- a/model/client4.go +++ b/model/client4.go @@ -4263,6 +4263,21 @@ func (c *Client4) uploadPlugin(file io.Reader, force bool) (*Manifest, *Response return ManifestFromJson(rp.Body), BuildResponse(rp) } +func (c *Client4) InstallPluginFromUrl(downloadUrl string, force bool) (*Manifest, *Response) { + forceStr := "false" + if force { + forceStr = "true" + } + + url := fmt.Sprintf("%s?plugin_download_url=%s&force=%s", c.GetPluginsRoute()+"/install_from_url", url.QueryEscape(downloadUrl), forceStr) + r, err := c.DoApiPost(url, "") + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return ManifestFromJson(r.Body), BuildResponse(r) +} + // GetPlugins will return a list of plugin manifests for currently active plugins. // WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. func (c *Client4) GetPlugins() (*PluginsResponse, *Response) { diff --git a/model/config.go b/model/config.go index 784a309986..addb3d1d0e 100644 --- a/model/config.go +++ b/model/config.go @@ -2183,12 +2183,13 @@ type PluginState struct { } type PluginSettings struct { - Enable *bool - EnableUploads *bool `restricted:"true"` - Directory *string `restricted:"true"` - ClientDirectory *string `restricted:"true"` - Plugins map[string]map[string]interface{} - PluginStates map[string]*PluginState + Enable *bool + EnableUploads *bool `restricted:"true"` + AllowInsecureDownloadUrl *bool `restricted:"true"` + Directory *string `restricted:"true"` + ClientDirectory *string `restricted:"true"` + Plugins map[string]map[string]interface{} + PluginStates map[string]*PluginState } func (s *PluginSettings) SetDefaults(ls LogSettings) { @@ -2200,6 +2201,10 @@ func (s *PluginSettings) SetDefaults(ls LogSettings) { s.EnableUploads = NewBool(false) } + if s.AllowInsecureDownloadUrl == nil { + s.AllowInsecureDownloadUrl = NewBool(false) + } + if s.Directory == nil { s.Directory = NewString(PLUGIN_SETTINGS_DEFAULT_DIRECTORY) }