From 899ab31fff9b34bc125faf75b79a89e390deb2cf Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Fri, 1 Sep 2017 09:00:27 -0400 Subject: [PATCH] Implement experimental REST API endpoints for plugins (#7279) * Implement experimental REST API endpoints for plugins * Updates per feedback and rebase * Update tests * Further updates * Update extraction of plugins * Use OS temp dir for plugins instead of search path * Fail extraction on paths that attempt to traverse upward * Update pluginenv ActivePlugins() --- api4/api.go | 7 ++ api4/context.go | 12 ++ api4/params.go | 5 + api4/plugin.go | 120 +++++++++++++++++++ api4/plugin_test.go | 115 ++++++++++++++++++ api4/system.go | 1 + app/plugins.go | 144 +++++++++++++++++++++++ app/server.go | 32 +++++ config/default.json | 1 + i18n/en.json | 60 ++++++++++ {plugin => model}/bundle_info.go | 2 +- {plugin => model}/bundle_info_test.go | 2 +- model/client4.go | 69 +++++++++++ model/config.go | 6 + {plugin => model}/manifest.go | 54 ++++++++- {plugin => model}/manifest_test.go | 35 +++++- plugin/pluginenv/environment.go | 161 ++++++++++++++++++++------ plugin/pluginenv/environment_test.go | 31 +++-- plugin/pluginenv/options.go | 12 +- plugin/pluginenv/options_test.go | 14 +-- plugin/pluginenv/search_path.go | 8 +- plugin/pluginenv/search_path_test.go | 10 +- plugin/rpcplugin/supervisor.go | 3 +- plugin/rpcplugin/supervisor_test.go | 8 +- tests/testplugin.tar.gz | Bin 0 -> 71959 bytes utils/extract.go | 83 +++++++++++++ utils/path.go | 15 +++ utils/path_test.go | 31 +++++ 28 files changed, 965 insertions(+), 76 deletions(-) create mode 100644 api4/plugin.go create mode 100644 api4/plugin_test.go rename {plugin => model}/bundle_info.go (96%) rename {plugin => model}/bundle_info_test.go (97%) rename {plugin => model}/manifest.go (53%) rename {plugin => model}/manifest_test.go (71%) create mode 100644 tests/testplugin.tar.gz create mode 100644 utils/extract.go create mode 100644 utils/path.go create mode 100644 utils/path_test.go diff --git a/api4/api.go b/api4/api.go index 8ed94c1934..3a4f2c412e 100644 --- a/api4/api.go +++ b/api4/api.go @@ -53,6 +53,9 @@ type Routes struct { Files *mux.Router // 'api/v4/files' File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}' + Plugins *mux.Router // 'api/v4/plugins' + Plugin *mux.Router // 'api/v4/plugins/{plugin_id:[A-Za-z0-9_-]+}' + PublicFile *mux.Router // 'files/{file_id:[A-Za-z0-9]+}/public' Commands *mux.Router // 'api/v4/commands' @@ -146,6 +149,9 @@ func InitApi(full bool) { BaseRoutes.File = BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter() BaseRoutes.PublicFile = BaseRoutes.Root.PathPrefix("/files/{file_id:[A-Za-z0-9]+}/public").Subrouter() + BaseRoutes.Plugins = BaseRoutes.ApiRoot.PathPrefix("/plugins").Subrouter() + BaseRoutes.Plugin = BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-]+}").Subrouter() + BaseRoutes.Commands = BaseRoutes.ApiRoot.PathPrefix("/commands").Subrouter() BaseRoutes.Command = BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter() @@ -205,6 +211,7 @@ func InitApi(full bool) { InitReaction() InitWebrtc() InitOpenGraph() + InitPlugin() app.Srv.Router.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404)) diff --git a/api4/context.go b/api4/context.go index 3ea67b30c4..2c0e54ea0c 100644 --- a/api4/context.go +++ b/api4/context.go @@ -428,6 +428,18 @@ func (c *Context) RequireFileId() *Context { return c } +func (c *Context) RequirePluginId() *Context { + if c.Err != nil { + return c + } + + if len(c.Params.PluginId) == 0 { + c.SetInvalidUrlParam("plugin_id") + } + + return c +} + func (c *Context) RequireReportId() *Context { if c.Err != nil { return c diff --git a/api4/params.go b/api4/params.go index 8b1d0febeb..1f0fe8e635 100644 --- a/api4/params.go +++ b/api4/params.go @@ -24,6 +24,7 @@ type ApiParams struct { ChannelId string PostId string FileId string + PluginId string CommandId string HookId string ReportId string @@ -78,6 +79,10 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams { params.FileId = val } + if val, ok := props["plugin_id"]; ok { + params.PluginId = val + } + if val, ok := props["command_id"]; ok { params.CommandId = val } diff --git a/api4/plugin.go b/api4/plugin.go new file mode 100644 index 0000000000..109695174a --- /dev/null +++ b/api4/plugin.go @@ -0,0 +1,120 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +// EXPERIMENTAL - SUBJECT TO CHANGE + +package api4 + +import ( + "net/http" + + l4g "github.com/alecthomas/log4go" + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +const ( + MAXIMUM_PLUGIN_FILE_SIZE = 50 * 1024 * 1024 +) + +func InitPlugin() { + l4g.Debug("EXPERIMENTAL: Initializing plugin api") + + BaseRoutes.Plugins.Handle("", ApiSessionRequired(uploadPlugin)).Methods("POST") + BaseRoutes.Plugins.Handle("", ApiSessionRequired(getPlugins)).Methods("GET") + BaseRoutes.Plugin.Handle("", ApiSessionRequired(removePlugin)).Methods("DELETE") + +} + +func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) { + if !*utils.Cfg.PluginSettings.Enable { + c.Err = model.NewAppError("uploadPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + return + } + + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + if err := r.ParseMultipartForm(MAXIMUM_PLUGIN_FILE_SIZE); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + m := r.MultipartForm + + pluginArray, ok := m.File["plugin"] + if !ok { + c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.no_file.app_error", nil, "", http.StatusBadRequest) + return + } + + if len(pluginArray) <= 0 { + c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.array.app_error", nil, "", http.StatusBadRequest) + return + } + + file, err := pluginArray[0].Open() + if err != nil { + c.Err = model.NewAppError("uploadPlugin", "api.plugin.upload.file.app_error", nil, "", http.StatusBadRequest) + return + } + defer file.Close() + + manifest, unpackErr := app.UnpackAndActivatePlugin(file) + + 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 !*utils.Cfg.PluginSettings.Enable { + c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + return + } + + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + manifests, err := app.GetActivePluginManifests() + if err != nil { + c.Err = err + return + } + + w.Write([]byte(model.ManifestListToJson(manifests))) +} + +func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequirePluginId() + if c.Err != nil { + return + } + + if !*utils.Cfg.PluginSettings.Enable { + c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + return + } + + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + err := app.RemovePlugin(c.Params.PluginId) + if err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) +} diff --git a/api4/plugin_test.go b/api4/plugin_test.go new file mode 100644 index 0000000000..c1d6c987cc --- /dev/null +++ b/api4/plugin_test.go @@ -0,0 +1,115 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "bytes" + "io/ioutil" + "os" + "testing" + + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPlugin(t *testing.T) { + pluginDir, err := ioutil.TempDir("", "mm-plugin-test") + require.NoError(t, err) + defer func() { + os.RemoveAll(pluginDir) + }() + webappDir, err := ioutil.TempDir("", "mm-webapp-test") + require.NoError(t, err) + defer func() { + os.RemoveAll(webappDir) + }() + + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + + app.StartupPlugins(pluginDir, webappDir) + + enablePlugins := *utils.Cfg.PluginSettings.Enable + defer func() { + *utils.Cfg.PluginSettings.Enable = enablePlugins + }() + *utils.Cfg.PluginSettings.Enable = true + + path, _ := utils.FindDir("tests") + file, err := os.Open(path + "/testplugin.tar.gz") + if err != nil { + t.Fatal(err) + } + defer file.Close() + + // Successful upload + manifest, resp := th.SystemAdminClient.UploadPlugin(file) + defer func() { + os.RemoveAll("plugins/testplugin") + }() + CheckNoError(t, resp) + + assert.Equal(t, "testplugin", manifest.Id) + + // Upload error cases + _, resp = th.SystemAdminClient.UploadPlugin(bytes.NewReader([]byte("badfile"))) + CheckBadRequestStatus(t, resp) + + *utils.Cfg.PluginSettings.Enable = false + _, resp = th.SystemAdminClient.UploadPlugin(file) + CheckNotImplementedStatus(t, resp) + + *utils.Cfg.PluginSettings.Enable = true + _, resp = th.Client.UploadPlugin(file) + CheckForbiddenStatus(t, resp) + + // Successful get + manifests, resp := th.SystemAdminClient.GetPlugins() + CheckNoError(t, resp) + + found := false + for _, m := range manifests { + if m.Id == manifest.Id { + found = true + } + } + + assert.True(t, found) + + // Get error cases + *utils.Cfg.PluginSettings.Enable = false + _, resp = th.SystemAdminClient.GetPlugins() + CheckNotImplementedStatus(t, resp) + + *utils.Cfg.PluginSettings.Enable = true + _, resp = th.Client.GetPlugins() + CheckForbiddenStatus(t, resp) + + // Successful remove + ok, resp := th.SystemAdminClient.RemovePlugin(manifest.Id) + CheckNoError(t, resp) + + assert.True(t, ok) + + // Remove error cases + ok, resp = th.SystemAdminClient.RemovePlugin(manifest.Id) + CheckBadRequestStatus(t, resp) + + assert.False(t, ok) + + *utils.Cfg.PluginSettings.Enable = false + _, resp = th.SystemAdminClient.RemovePlugin(manifest.Id) + CheckNotImplementedStatus(t, resp) + + *utils.Cfg.PluginSettings.Enable = true + _, resp = th.Client.RemovePlugin(manifest.Id) + CheckForbiddenStatus(t, resp) + + _, resp = th.SystemAdminClient.RemovePlugin("bad.id") + CheckNotFoundStatus(t, resp) + + app.Srv.PluginEnv = nil +} diff --git a/api4/system.go b/api4/system.go index 2ad408e13c..8f98afedb9 100644 --- a/api4/system.go +++ b/api4/system.go @@ -244,6 +244,7 @@ func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) { } respCfg["NoAccounts"] = strconv.FormatBool(app.IsFirstUserAccount()) + respCfg["Plugins"] = app.GetPluginsForClientConfig() w.Write([]byte(model.MapToJson(respCfg))) } diff --git a/app/plugins.go b/app/plugins.go index 82eda067c3..51f6414a33 100644 --- a/app/plugins.go +++ b/app/plugins.go @@ -5,7 +5,12 @@ package app import ( "encoding/json" + "io" + "io/ioutil" "net/http" + "os" + "path/filepath" + "strings" l4g "github.com/alecthomas/log4go" @@ -84,3 +89,142 @@ func InitPlugins() { p.OnConfigurationChange() } } + +func ActivatePlugins() { + if Srv.PluginEnv == nil { + l4g.Error("plugin env not initialized") + return + } + + plugins, err := Srv.PluginEnv.Plugins() + if err != nil { + l4g.Error("failed to start up plugins: " + err.Error()) + return + } + + for _, plugin := range plugins { + err := Srv.PluginEnv.ActivatePlugin(plugin.Manifest.Id) + if err != nil { + l4g.Error(err.Error()) + } + l4g.Info("Activated %v plugin", plugin.Manifest.Id) + } +} + +func UnpackAndActivatePlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) { + if Srv.PluginEnv == nil || !*utils.Cfg.PluginSettings.Enable { + return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + tmpDir, err := ioutil.TempDir("", "plugintmp") + if err != nil { + return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.temp_dir.app_error", nil, err.Error(), http.StatusInternalServerError) + } + defer func() { + os.RemoveAll(tmpDir) + }() + + filenames, err := utils.ExtractTarGz(pluginFile, tmpDir) + if err != nil { + return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if len(filenames) == 0 { + return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.no_files.app_error", nil, err.Error(), http.StatusBadRequest) + } + + splitPath := strings.Split(filenames[0], string(os.PathSeparator)) + + if len(splitPath) == 0 { + return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.bad_path.app_error", nil, err.Error(), http.StatusBadRequest) + } + + manifestDir := filepath.Join(tmpDir, splitPath[0]) + + manifest, _, err := model.FindManifest(manifestDir) + if err != nil { + return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest) + } + + os.Rename(manifestDir, filepath.Join(Srv.PluginEnv.SearchPath(), manifest.Id)) + if err != nil { + return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + // Should add manifest validation and error handling here + + err = Srv.PluginEnv.ActivatePlugin(manifest.Id) + if err != nil { + return nil, model.NewAppError("UnpackAndActivatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest) + } + + return manifest, nil +} + +func GetActivePluginManifests() ([]*model.Manifest, *model.AppError) { + if Srv.PluginEnv == nil || !*utils.Cfg.PluginSettings.Enable { + return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + plugins, err := Srv.PluginEnv.ActivePlugins() + if err != nil { + return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + manifests := make([]*model.Manifest, len(plugins)) + for i, plugin := range plugins { + manifests[i] = plugin.Manifest + } + + return manifests, nil +} + +func RemovePlugin(id string) *model.AppError { + if Srv.PluginEnv == nil || !*utils.Cfg.PluginSettings.Enable { + return model.NewAppError("RemovePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + err := Srv.PluginEnv.DeactivatePlugin(id) + if err != nil { + return model.NewAppError("RemovePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest) + } + + err = os.RemoveAll(filepath.Join(Srv.PluginEnv.SearchPath(), id)) + if err != nil { + return model.NewAppError("RemovePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return nil +} + +// Temporary WIP function/type for experimental webapp plugins +type ClientConfigPlugin struct { + Id string `json:"id"` + BundlePath string `json:"bundle_path"` +} + +func GetPluginsForClientConfig() string { + if Srv.PluginEnv == nil || !*utils.Cfg.PluginSettings.Enable { + return "" + } + + plugins, err := Srv.PluginEnv.ActivePlugins() + if err != nil { + return "" + } + + pluginsConfig := []ClientConfigPlugin{} + for _, plugin := range plugins { + if plugin.Manifest.Webapp == nil { + continue + } + pluginsConfig = append(pluginsConfig, ClientConfigPlugin{Id: plugin.Manifest.Id, BundlePath: plugin.Manifest.Webapp.BundlePath}) + } + + b, err := json.Marshal(pluginsConfig) + if err != nil { + return "" + } + + return string(b) +} diff --git a/app/server.go b/app/server.go index b83aa95068..c3bcd562d9 100644 --- a/app/server.go +++ b/app/server.go @@ -7,6 +7,7 @@ import ( "crypto/tls" "net" "net/http" + "os" "strings" "time" @@ -19,6 +20,7 @@ import ( "gopkg.in/throttled/throttled.v2/store/memstore" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/plugin/pluginenv" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" ) @@ -28,6 +30,7 @@ type Server struct { WebSocketRouter *WebSocketRouter Router *mux.Router GracefulServer *graceful.Server + PluginEnv *pluginenv.Environment } var allowedMethods []string = []string{ @@ -186,6 +189,10 @@ func StartServer() { }() } + if *utils.Cfg.PluginSettings.Enable { + StartupPlugins("plugins", "webapp/dist") + } + go func() { var err error if *utils.Cfg.ServiceSettings.ConnectionSecurity == model.CONN_SECURITY_TLS { @@ -223,3 +230,28 @@ func StopServer() { l4g.Info(utils.T("api.server.stop_server.stopped.info")) } + +func StartupPlugins(pluginPath, webappPath string) { + l4g.Info("Starting up plugins") + + err := os.Mkdir(pluginPath, 0744) + if err != nil { + if os.IsExist(err) { + err = nil + } else { + l4g.Error("failed to start up plugins: " + err.Error()) + return + } + } + + Srv.PluginEnv, err = pluginenv.New( + pluginenv.SearchPath(pluginPath), + pluginenv.WebappPath(webappPath), + ) + + if err != nil { + l4g.Error("failed to start up plugins: " + err.Error()) + } + + ActivatePlugins() +} diff --git a/config/default.json b/config/default.json index e0d3ec53c5..1c772c4ff5 100644 --- a/config/default.json +++ b/config/default.json @@ -314,6 +314,7 @@ "RunScheduler": true }, "PluginSettings": { + "Enable": false, "Plugins": {} } } diff --git a/i18n/en.json b/i18n/en.json index af93ef775f..794424affb 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -403,6 +403,62 @@ "id": "api.command.delete.app_error", "translation": "Invalid permissions to delete command" }, + { + "id": "app.plugin.disabled.app_error", + "translation": "Plugins have been disabled by the system admin or the server has not been restarted since they were enabled." + }, + { + "id": "app.plugin.disabled.app_error", + "translation": "" + }, + { + "id": "app.plugin.extract.app_error", + "translation": "Encountered error extracting plugin" + }, + { + "id": "app.plugin.no_files.app_error", + "translation": "No files found in the compressed folder" + }, + { + "id": "app.plugin.bad_path.app_error", + "translation": "Bad file path in extracted files" + }, + { + "id": "app.plugin.manifest.app_error", + "translation": "Unable to find manifest for extracted plugin" + }, + { + "id": "app.plugin.mvdir.app_error", + "translation": "Unable to move plugin from temporary directory to final destination" + }, + { + "id": "app.plugin.activate.app_error", + "translation": "Unable to activate extracted plugin. Plugin may already exist and be activated." + }, + { + "id": "app.plugin.get_plugins.app_error", + "translation": "Unable to get active plugins" + }, + { + "id": "app.plugin.deactivate.app_error", + "translation": "Unable to deactivate plugin" + }, + { + "id": "app.plugin.remove.app_error", + "translation": "Unable to delete plugin" + }, + { + "id": "api.plugin.upload.no_file.app_error", + "translation": "Missing file in multipart/form request" + }, + { + "id": "api.plugin.upload.array.app_error", + "translation": "File array is empty in multipart/form request" + }, + { + "id": "api.plugin.upload.file.app_error", + "translation": "Unable to open file in multipart/form request" + }, { "id": "api.command.disabled.app_error", "translation": "Commands have been disabled by the system admin." @@ -4187,6 +4243,10 @@ "id": "model.channel_member.is_valid.user_id.app_error", "translation": "Invalid user id" }, + { + "id": "model.client.writer.app_error", + "translation": "Unable to build multipart request" + }, { "id": "model.client.connecting.app_error", "translation": "We encountered an error while connecting to the server" diff --git a/plugin/bundle_info.go b/model/bundle_info.go similarity index 96% rename from plugin/bundle_info.go rename to model/bundle_info.go index 9dc47ceea1..67b5dd0ed7 100644 --- a/plugin/bundle_info.go +++ b/model/bundle_info.go @@ -1,4 +1,4 @@ -package plugin +package model type BundleInfo struct { Path string diff --git a/plugin/bundle_info_test.go b/model/bundle_info_test.go similarity index 97% rename from plugin/bundle_info_test.go rename to model/bundle_info_test.go index 94a0c624fa..e94a5cb64d 100644 --- a/plugin/bundle_info_test.go +++ b/model/bundle_info_test.go @@ -1,4 +1,4 @@ -package plugin +package model import ( "io/ioutil" diff --git a/model/client4.go b/model/client4.go index 26ea6ee03f..badb60a2a4 100644 --- a/model/client4.go +++ b/model/client4.go @@ -178,6 +178,14 @@ func (c *Client4) GetFileRoute(fileId string) string { return fmt.Sprintf(c.GetFilesRoute()+"/%v", fileId) } +func (c *Client4) GetPluginsRoute() string { + return fmt.Sprintf("/plugins") +} + +func (c *Client4) GetPluginRoute(pluginId string) string { + return fmt.Sprintf(c.GetPluginsRoute()+"/%v", pluginId) +} + func (c *Client4) GetSystemRoute() string { return fmt.Sprintf("/system") } @@ -3019,3 +3027,64 @@ func (c *Client4) CancelJob(jobId string) (bool, *Response) { return CheckStatusOK(r), BuildResponse(r) } } + +// Plugin Section + +// UploadPlugin takes an io.Reader stream pointing to the contents of a .tar.gz plugin. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) UploadPlugin(file io.Reader) (*Manifest, *Response) { + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + + if part, err := writer.CreateFormFile("plugin", "plugin.tar.gz"); err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)} + } else if _, err = io.Copy(part, file); err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)} + } + + if err := writer.Close(); err != nil { + return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)} + } + + rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetPluginsRoute(), body) + rq.Header.Set("Content-Type", writer.FormDataContentType()) + rq.Close = true + + if len(c.AuthToken) > 0 { + rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken) + } + + if rp, err := c.HttpClient.Do(rq); err != nil || rp == nil { + return nil, BuildErrorResponse(rp, NewAppError("UploadPlugin", "model.client.connecting.app_error", nil, err.Error(), 0)) + } else { + defer closeBody(rp) + + if rp.StatusCode >= 300 { + return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body)) + } else { + return ManifestFromJson(rp.Body), BuildResponse(rp) + } + } +} + +// 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() ([]*Manifest, *Response) { + if r, err := c.DoApiGet(c.GetPluginsRoute(), ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return ManifestListFromJson(r.Body), BuildResponse(r) + } +} + +// RemovePlugin will deactivate and delete a plugin. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) RemovePlugin(id string) (bool, *Response) { + if r, err := c.DoApiDelete(c.GetPluginRoute(id)); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} diff --git a/model/config.go b/model/config.go index 050110512f..65608c9a52 100644 --- a/model/config.go +++ b/model/config.go @@ -477,6 +477,7 @@ type JobSettings struct { } type PluginSettings struct { + Enable *bool Plugins map[string]interface{} } @@ -1522,6 +1523,11 @@ func (o *Config) SetDefaults() { *o.JobSettings.RunScheduler = true } + if o.PluginSettings.Enable == nil { + o.PluginSettings.Enable = new(bool) + *o.PluginSettings.Enable = false + } + if o.PluginSettings.Plugins == nil { o.PluginSettings.Plugins = make(map[string]interface{}) } diff --git a/plugin/manifest.go b/model/manifest.go similarity index 53% rename from plugin/manifest.go rename to model/manifest.go index 15b7f05550..e61ccc8ad5 100644 --- a/plugin/manifest.go +++ b/model/manifest.go @@ -1,7 +1,8 @@ -package plugin +package model import ( "encoding/json" + "io" "io/ioutil" "os" "path/filepath" @@ -10,14 +11,61 @@ import ( ) type Manifest struct { - Id string `json:"id" yaml:"id"` - Backend *ManifestBackend `json:"backend,omitempty" yaml:"backend,omitempty"` + Id string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + Backend *ManifestBackend `json:"backend,omitempty" yaml:"backend,omitempty"` + Webapp *ManifestWebapp `json:"webapp,omitempty" yaml:"webapp,omitempty"` } type ManifestBackend struct { Executable string `json:"executable" yaml:"executable"` } +type ManifestWebapp struct { + BundlePath string `json:"bundle_path" yaml:"bundle_path"` +} + +func (m *Manifest) ToJson() string { + b, err := json.Marshal(m) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ManifestListToJson(m []*Manifest) string { + b, err := json.Marshal(m) + if err != nil { + return "" + } else { + return string(b) + } +} + +func ManifestFromJson(data io.Reader) *Manifest { + decoder := json.NewDecoder(data) + var m Manifest + err := decoder.Decode(&m) + if err == nil { + return &m + } else { + return nil + } +} + +func ManifestListFromJson(data io.Reader) []*Manifest { + decoder := json.NewDecoder(data) + var manifests []*Manifest + err := decoder.Decode(&manifests) + if err == nil { + return manifests + } else { + return nil + } +} + // FindManifest will find and parse the manifest in a given directory. // // In all cases other than a does-not-exist error, path is set to the path of the manifest file that was diff --git a/plugin/manifest_test.go b/model/manifest_test.go similarity index 71% rename from plugin/manifest_test.go rename to model/manifest_test.go index 5dae4fbaa3..237640564f 100644 --- a/plugin/manifest_test.go +++ b/model/manifest_test.go @@ -1,4 +1,4 @@ -package plugin +package model import ( "encoding/json" @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -59,6 +60,9 @@ func TestManifestUnmarshal(t *testing.T) { Backend: &ManifestBackend{ Executable: "theexecutable", }, + Webapp: &ManifestWebapp{ + BundlePath: "thebundlepath", + }, } var yamlResult Manifest @@ -66,6 +70,8 @@ func TestManifestUnmarshal(t *testing.T) { id: theid backend: executable: theexecutable +webapp: + bundle_path: thebundlepath `), &yamlResult)) assert.Equal(t, expected, yamlResult) @@ -74,6 +80,9 @@ backend: "id": "theid", "backend": { "executable": "theexecutable" + }, + "webapp": { + "bundle_path": "thebundlepath" } }`), &jsonResult)) assert.Equal(t, expected, jsonResult) @@ -95,3 +104,27 @@ func TestFindManifest_FileErrors(t *testing.T) { assert.False(t, os.IsNotExist(err), tc) } } + +func TestManifestJson(t *testing.T) { + manifest := &Manifest{ + Id: "theid", + Backend: &ManifestBackend{ + Executable: "theexecutable", + }, + Webapp: &ManifestWebapp{ + BundlePath: "thebundlepath", + }, + } + + json := manifest.ToJson() + newManifest := ManifestFromJson(strings.NewReader(json)) + assert.Equal(t, newManifest, manifest) + assert.Equal(t, newManifest.ToJson(), json) + assert.Equal(t, ManifestFromJson(strings.NewReader("junk")), (*Manifest)(nil)) + + manifestList := []*Manifest{manifest} + json = ManifestListToJson(manifestList) + newManifestList := ManifestListFromJson(strings.NewReader(json)) + assert.Equal(t, newManifestList, manifestList) + assert.Equal(t, ManifestListToJson(newManifestList), json) +} diff --git a/plugin/pluginenv/environment.go b/plugin/pluginenv/environment.go index 36a8c6e769..ebda0e0db1 100644 --- a/plugin/pluginenv/environment.go +++ b/plugin/pluginenv/environment.go @@ -3,21 +3,31 @@ package pluginenv import ( "fmt" + "io/ioutil" + "sync" "github.com/pkg/errors" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/plugin" ) -type APIProviderFunc func(*plugin.Manifest) (plugin.API, error) -type SupervisorProviderFunc func(*plugin.BundleInfo) (plugin.Supervisor, error) +type APIProviderFunc func(*model.Manifest) (plugin.API, error) +type SupervisorProviderFunc func(*model.BundleInfo) (plugin.Supervisor, error) + +type ActivePlugin struct { + BundleInfo *model.BundleInfo + Supervisor plugin.Supervisor +} // Environment represents an environment that plugins are discovered and launched in. type Environment struct { searchPath string + webappPath string apiProvider APIProviderFunc supervisorProvider SupervisorProviderFunc - activePlugins map[string]plugin.Supervisor + activePlugins map[string]ActivePlugin + mutex sync.Mutex } type Option func(*Environment) @@ -25,7 +35,7 @@ type Option func(*Environment) // Creates a new environment. At a minimum, the APIProvider and SearchPath options are required. func New(options ...Option) (*Environment, error) { env := &Environment{ - activePlugins: make(map[string]plugin.Supervisor), + activePlugins: make(map[string]ActivePlugin), } for _, opt := range options { opt(env) @@ -35,19 +45,45 @@ func New(options ...Option) (*Environment, error) { } if env.searchPath == "" { return nil, fmt.Errorf("a search path must be provided") - } else if env.apiProvider == nil { - return nil, fmt.Errorf("an api provider must be provided") } return env, nil } +// Returns the configured webapp path. +func (env *Environment) WebappPath() string { + return env.webappPath +} + +// Returns the configured search path. +func (env *Environment) SearchPath() string { + return env.searchPath +} + // Returns a list of all plugins found within the environment. -func (env *Environment) Plugins() ([]*plugin.BundleInfo, error) { +func (env *Environment) Plugins() ([]*model.BundleInfo, error) { + env.mutex.Lock() + defer env.mutex.Unlock() return ScanSearchPath(env.searchPath) } +// Returns a list of all currently active plugins within the environment. +func (env *Environment) ActivePlugins() ([]*model.BundleInfo, error) { + env.mutex.Lock() + defer env.mutex.Unlock() + + activePlugins := []*model.BundleInfo{} + for _, p := range env.activePlugins { + activePlugins = append(activePlugins, p.BundleInfo) + } + + return activePlugins, nil +} + // Returns the ids of the currently active plugins. func (env *Environment) ActivePluginIds() (ids []string) { + env.mutex.Lock() + defer env.mutex.Unlock() + for id := range env.activePlugins { ids = append(ids, id) } @@ -56,6 +92,9 @@ func (env *Environment) ActivePluginIds() (ids []string) { // Activates the plugin with the given id. func (env *Environment) ActivatePlugin(id string) error { + env.mutex.Lock() + defer env.mutex.Unlock() + if _, ok := env.activePlugins[id]; ok { return fmt.Errorf("plugin already active: %v", id) } @@ -63,46 +102,91 @@ func (env *Environment) ActivatePlugin(id string) error { if err != nil { return err } - var plugin *plugin.BundleInfo + var bundle *model.BundleInfo for _, p := range plugins { if p.Manifest != nil && p.Manifest.Id == id { - if plugin != nil { + if bundle != nil { return fmt.Errorf("multiple plugins found: %v", id) } - plugin = p + bundle = p } } - if plugin == nil { + if bundle == nil { return fmt.Errorf("plugin not found: %v", id) } - supervisor, err := env.supervisorProvider(plugin) - if err != nil { - return errors.Wrapf(err, "unable to create supervisor for plugin: %v", id) + + activePlugin := ActivePlugin{BundleInfo: bundle} + + var supervisor plugin.Supervisor + + if bundle.Manifest.Backend != nil { + if env.apiProvider == nil { + return fmt.Errorf("env missing api provider, cannot activate plugin: %v", id) + } + + supervisor, err = env.supervisorProvider(bundle) + if err != nil { + return errors.Wrapf(err, "unable to create supervisor for plugin: %v", id) + } + api, err := env.apiProvider(bundle.Manifest) + if err != nil { + return errors.Wrapf(err, "unable to get api for plugin: %v", id) + } + if err := supervisor.Start(); err != nil { + return errors.Wrapf(err, "unable to start plugin: %v", id) + } + if err := supervisor.Hooks().OnActivate(api); err != nil { + supervisor.Stop() + return errors.Wrapf(err, "unable to activate plugin: %v", id) + } + + activePlugin.Supervisor = supervisor } - api, err := env.apiProvider(plugin.Manifest) - if err != nil { - return errors.Wrapf(err, "unable to get api for plugin: %v", id) + + if bundle.Manifest.Webapp != nil { + if env.webappPath == "" { + if supervisor != nil { + supervisor.Stop() + } + return fmt.Errorf("env missing webapp path, cannot activate plugin: %v", id) + } + + webappBundle, err := ioutil.ReadFile(fmt.Sprintf("%s/%s/webapp/%s_bundle.js", env.searchPath, id, id)) + if err != nil { + if supervisor != nil { + supervisor.Stop() + } + return errors.Wrapf(err, "unable to read webapp bundle: %v", id) + } + + err = ioutil.WriteFile(fmt.Sprintf("%s/%s_bundle.js", env.webappPath, id), webappBundle, 0644) + if err != nil { + if supervisor != nil { + supervisor.Stop() + } + return errors.Wrapf(err, "unable to write webapp bundle: %v", id) + } } - if err := supervisor.Start(); err != nil { - return errors.Wrapf(err, "unable to start plugin: %v", id) - } - if err := supervisor.Hooks().OnActivate(api); err != nil { - supervisor.Stop() - return errors.Wrapf(err, "unable to activate plugin: %v", id) - } - env.activePlugins[id] = supervisor + + env.activePlugins[id] = activePlugin return nil } // Deactivates the plugin with the given id. func (env *Environment) DeactivatePlugin(id string) error { - if supervisor, ok := env.activePlugins[id]; !ok { + env.mutex.Lock() + defer env.mutex.Unlock() + + if activePlugin, ok := env.activePlugins[id]; !ok { return fmt.Errorf("plugin not active: %v", id) } else { delete(env.activePlugins, id) - err := supervisor.Hooks().OnDeactivate() - if serr := supervisor.Stop(); err == nil { - err = serr + var err error + if activePlugin.Supervisor != nil { + err = activePlugin.Supervisor.Hooks().OnDeactivate() + if serr := activePlugin.Supervisor.Stop(); err == nil { + err = serr + } } return err } @@ -110,14 +194,19 @@ func (env *Environment) DeactivatePlugin(id string) error { // Deactivates all plugins and gracefully shuts down the environment. func (env *Environment) Shutdown() (errs []error) { - for _, supervisor := range env.activePlugins { - if err := supervisor.Hooks().OnDeactivate(); err != nil { - errs = append(errs, err) - } - if err := supervisor.Stop(); err != nil { - errs = append(errs, err) + env.mutex.Lock() + defer env.mutex.Unlock() + + for _, activePlugin := range env.activePlugins { + if activePlugin.Supervisor != nil { + if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil { + errs = append(errs, err) + } + if err := activePlugin.Supervisor.Stop(); err != nil { + errs = append(errs, err) + } } } - env.activePlugins = make(map[string]plugin.Supervisor) + env.activePlugins = make(map[string]ActivePlugin) return } diff --git a/plugin/pluginenv/environment_test.go b/plugin/pluginenv/environment_test.go index d933c86961..82086b9b6e 100644 --- a/plugin/pluginenv/environment_test.go +++ b/plugin/pluginenv/environment_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/plugin" "github.com/mattermost/platform/plugin/plugintest" ) @@ -19,7 +20,7 @@ type MockProvider struct { mock.Mock } -func (m *MockProvider) API(manifest *plugin.Manifest) (plugin.API, error) { +func (m *MockProvider) API(manifest *model.Manifest) (plugin.API, error) { ret := m.Called() if ret.Get(0) == nil { return nil, ret.Error(1) @@ -27,7 +28,7 @@ func (m *MockProvider) API(manifest *plugin.Manifest) (plugin.API, error) { return ret.Get(0).(plugin.API), ret.Error(1) } -func (m *MockProvider) Supervisor(bundle *plugin.BundleInfo) (plugin.Supervisor, error) { +func (m *MockProvider) Supervisor(bundle *model.BundleInfo) (plugin.Supervisor, error) { ret := m.Called() if ret.Get(0) == nil { return nil, ret.Error(1) @@ -90,19 +91,13 @@ func TestNew_MissingOptions(t *testing.T) { ) assert.Nil(t, env) assert.Error(t, err) - - env, err = New( - SearchPath(dir), - ) - assert.Nil(t, env) - assert.Error(t, err) } func TestEnvironment(t *testing.T) { dir := initTmpDir(t, map[string]string{ ".foo/plugin.json": `{"id": "foo"}`, "foo/bar": "asdf", - "foo/plugin.json": `{"id": "foo"}`, + "foo/plugin.json": `{"id": "foo", "backend": {}}`, "bar/zxc": "qwer", "baz/plugin.yaml": "id: baz", "bad/plugin.json": "asd", @@ -110,11 +105,14 @@ func TestEnvironment(t *testing.T) { }) defer os.RemoveAll(dir) + webappDir := "notarealdirectory" + var provider MockProvider defer provider.AssertExpectations(t) env, err := New( SearchPath(dir), + WebappPath(webappDir), APIProvider(provider.API), SupervisorProvider(provider.Supervisor), ) @@ -125,6 +123,10 @@ func TestEnvironment(t *testing.T) { assert.NoError(t, err) assert.Len(t, plugins, 3) + activePlugins, err := env.ActivePlugins() + assert.NoError(t, err) + assert.Len(t, activePlugins, 0) + assert.Error(t, env.ActivatePlugin("x")) var api struct{ plugin.API } @@ -144,6 +146,9 @@ func TestEnvironment(t *testing.T) { assert.NoError(t, env.ActivatePlugin("foo")) assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) + activePlugins, err = env.ActivePlugins() + assert.NoError(t, err) + assert.Len(t, activePlugins, 1) assert.Error(t, env.ActivatePlugin("foo")) hooks.On("OnDeactivate").Return(nil) @@ -152,6 +157,10 @@ func TestEnvironment(t *testing.T) { assert.NoError(t, env.ActivatePlugin("foo")) assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) + + assert.Equal(t, env.SearchPath(), dir) + assert.Equal(t, env.WebappPath(), webappDir) + assert.Empty(t, env.Shutdown()) } @@ -195,7 +204,7 @@ func TestEnvironment_BadSearchPathError(t *testing.T) { func TestEnvironment_ActivatePluginErrors(t *testing.T) { dir := initTmpDir(t, map[string]string{ - "foo/plugin.json": `{"id": "foo"}`, + "foo/plugin.json": `{"id": "foo", "backend": {}}`, }) defer os.RemoveAll(dir) @@ -254,7 +263,7 @@ func TestEnvironment_ActivatePluginErrors(t *testing.T) { func TestEnvironment_ShutdownError(t *testing.T) { dir := initTmpDir(t, map[string]string{ - "foo/plugin.json": `{"id": "foo"}`, + "foo/plugin.json": `{"id": "foo", "backend": {}}`, }) defer os.RemoveAll(dir) diff --git a/plugin/pluginenv/options.go b/plugin/pluginenv/options.go index 3f83228fb3..e5ef9678d8 100644 --- a/plugin/pluginenv/options.go +++ b/plugin/pluginenv/options.go @@ -3,6 +3,7 @@ package pluginenv import ( "fmt" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/plugin" "github.com/mattermost/platform/plugin/rpcplugin" ) @@ -29,14 +30,21 @@ func SearchPath(path string) Option { } } +// WebappPath specifies the static directory serving the webapp. +func WebappPath(path string) Option { + return func(env *Environment) { + env.webappPath = path + } +} + // DefaultSupervisorProvider chooses a supervisor based on the plugin's manifest contents. E.g. if // the manifest specifies a backend executable, it will be given an rpcplugin.Supervisor. -func DefaultSupervisorProvider(bundle *plugin.BundleInfo) (plugin.Supervisor, error) { +func DefaultSupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) { if bundle.Manifest == nil { return nil, fmt.Errorf("a manifest is required") } if bundle.Manifest.Backend == nil { - return nil, fmt.Errorf("invalid manifest: at this time, only backend plugins are supported") + return nil, fmt.Errorf("invalid manifest: missing backend plugin") } return rpcplugin.SupervisorProvider(bundle) } diff --git a/plugin/pluginenv/options_test.go b/plugin/pluginenv/options_test.go index 4f8d411bd7..073d1861e1 100644 --- a/plugin/pluginenv/options_test.go +++ b/plugin/pluginenv/options_test.go @@ -6,22 +6,22 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/plugin/rpcplugin" ) func TestDefaultSupervisorProvider(t *testing.T) { - _, err := DefaultSupervisorProvider(&plugin.BundleInfo{}) + _, err := DefaultSupervisorProvider(&model.BundleInfo{}) assert.Error(t, err) - _, err = DefaultSupervisorProvider(&plugin.BundleInfo{ - Manifest: &plugin.Manifest{}, + _, err = DefaultSupervisorProvider(&model.BundleInfo{ + Manifest: &model.Manifest{}, }) assert.Error(t, err) - supervisor, err := DefaultSupervisorProvider(&plugin.BundleInfo{ - Manifest: &plugin.Manifest{ - Backend: &plugin.ManifestBackend{ + supervisor, err := DefaultSupervisorProvider(&model.BundleInfo{ + Manifest: &model.Manifest{ + Backend: &model.ManifestBackend{ Executable: "foo", }, }, diff --git a/plugin/pluginenv/search_path.go b/plugin/pluginenv/search_path.go index daebdb0d3d..b50c7019c3 100644 --- a/plugin/pluginenv/search_path.go +++ b/plugin/pluginenv/search_path.go @@ -4,7 +4,7 @@ import ( "io/ioutil" "path/filepath" - "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/model" ) // Performs a full scan of the given path. @@ -14,17 +14,17 @@ import ( // parsed). // // Plugins are found non-recursively and paths beginning with a dot are always ignored. -func ScanSearchPath(path string) ([]*plugin.BundleInfo, error) { +func ScanSearchPath(path string) ([]*model.BundleInfo, error) { files, err := ioutil.ReadDir(path) if err != nil { return nil, err } - var ret []*plugin.BundleInfo + var ret []*model.BundleInfo for _, file := range files { if !file.IsDir() || file.Name()[0] == '.' { continue } - if info := plugin.BundleInfoForPath(filepath.Join(path, file.Name())); info.ManifestPath != "" { + if info := model.BundleInfoForPath(filepath.Join(path, file.Name())); info.ManifestPath != "" { ret = append(ret, info) } } diff --git a/plugin/pluginenv/search_path_test.go b/plugin/pluginenv/search_path_test.go index d9a18cf56b..f8243e5e45 100644 --- a/plugin/pluginenv/search_path_test.go +++ b/plugin/pluginenv/search_path_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/model" ) func TestScanSearchPath(t *testing.T) { @@ -27,17 +27,17 @@ func TestScanSearchPath(t *testing.T) { plugins, err := ScanSearchPath(dir) require.NoError(t, err) assert.Len(t, plugins, 3) - assert.Contains(t, plugins, &plugin.BundleInfo{ + assert.Contains(t, plugins, &model.BundleInfo{ Path: filepath.Join(dir, "foo"), ManifestPath: filepath.Join(dir, "foo", "plugin.json"), - Manifest: &plugin.Manifest{ + Manifest: &model.Manifest{ Id: "foo", }, }) - assert.Contains(t, plugins, &plugin.BundleInfo{ + assert.Contains(t, plugins, &model.BundleInfo{ Path: filepath.Join(dir, "baz"), ManifestPath: filepath.Join(dir, "baz", "plugin.yaml"), - Manifest: &plugin.Manifest{ + Manifest: &model.Manifest{ Id: "baz", }, }) diff --git a/plugin/rpcplugin/supervisor.go b/plugin/rpcplugin/supervisor.go index 9316d71864..7abcca0fca 100644 --- a/plugin/rpcplugin/supervisor.go +++ b/plugin/rpcplugin/supervisor.go @@ -7,6 +7,7 @@ import ( "sync/atomic" "time" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/plugin" ) @@ -116,7 +117,7 @@ func (s *Supervisor) runPlugin(ctx context.Context, start chan<- error) error { return nil } -func SupervisorProvider(bundle *plugin.BundleInfo) (plugin.Supervisor, error) { +func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) { if bundle.Manifest == nil { return nil, fmt.Errorf("no manifest available") } else if bundle.Manifest.Backend == nil || bundle.Manifest.Backend.Executable == "" { diff --git a/plugin/rpcplugin/supervisor_test.go b/plugin/rpcplugin/supervisor_test.go index c43fd3dc9d..014d0dd391 100644 --- a/plugin/rpcplugin/supervisor_test.go +++ b/plugin/rpcplugin/supervisor_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/model" ) func TestSupervisor(t *testing.T) { @@ -35,7 +35,7 @@ func TestSupervisor(t *testing.T) { ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) - bundle := plugin.BundleInfoForPath(dir) + bundle := model.BundleInfoForPath(dir) supervisor, err := SupervisorProvider(bundle) require.NoError(t, err) require.NoError(t, supervisor.Start()) @@ -61,7 +61,7 @@ func TestSupervisor_StartTimeout(t *testing.T) { ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) - bundle := plugin.BundleInfoForPath(dir) + bundle := model.BundleInfoForPath(dir) supervisor, err := SupervisorProvider(bundle) require.NoError(t, err) require.Error(t, supervisor.Start()) @@ -98,7 +98,7 @@ func TestSupervisor_PluginCrash(t *testing.T) { ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) - bundle := plugin.BundleInfoForPath(dir) + bundle := model.BundleInfoForPath(dir) supervisor, err := SupervisorProvider(bundle) require.NoError(t, err) require.NoError(t, supervisor.Start()) diff --git a/tests/testplugin.tar.gz b/tests/testplugin.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..1852064fa1ba98f6483c3428ae8c5e1ec02ed7cf GIT binary patch literal 71959 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%0D_knjgjuq+2HM5SnLExcW149|LU zx4B~;X3=#QUdXEj_ZZzcDf#BuxhyBH+m0;7h+blbOjW4Tm5DsQ^kST`6gwp!RkFKx z({T>#>}V-XX_U$?Ur|GMwqg_xPWQLZ7RO$_S)kCqk2M zFOb%bA8#(nXp_O5uF9pE{Aaov754j_*Pcng=MjeR(~A&4AC5JviCBa`OH0C@>F^sQ z(OI3!BV)iDTqhK?oXqUjNDSdyfB6gDOWb|^=GBv({@Wja`0>?GKlZmscw=#i-p09zgWU`2icHw;#g~=<}@IYU#zlYt*dC5=@}}oOiy&N|9?hki8G&xhwEX z9uGCzlWq9482__1BR5d6RH!)_GI#k*IEs`9gI<#boVirG#qd&CjtkZLtf$q z@m?}I=a{(SRSaEh5fw4GV{nE2+Nc>fVaE|)+QYmsNs=z0R5nEKf~*%ZDFAX!)>MFlDD@qu(*e~e6p-o>A&n*mifdLRGi+BO81-7jH$ZQ=wGTZWClSb zmNb~GFk*`ych^BGJ~d~|at&}wH(NmiGD6ivaaW0m9~Q7a<)j!p;y}qVZA>;|-K*V` zsXk1}ag)w4Y|zb7w2hiCK#>+jMFt$MQN>78uuRM7I=b(4jeDLnmW&NJ>w;%kq2JDm z$f>TCZUk~RC)b@GLGx*dYt*|&`aGh?AXd{p9)xcq>}KMQfoxwKkJ*c3s1=RgKUSw& zY;;H|UJ9}~E#T+uh9H<5lrg>w6d-978Nb zRpA<3?XOv+Hyy7EM3qSjkmZVd#XMaXeGb1rx`f_Gn18ScOZ7h1emrdRH`s}l2BVt| zCj$=2+rZzBvw?qou6byGuLqo8!~h^h@v8eN{&39`zAC(V!WN#r>YLlbsrre!_^iOH z$ld~e)nMIF9GuGOky@Pg)UKdVrCLSJG}bwltzg=v6t~_2qk1T?RaVwD@!eBWSn03A z2g`1Lzy`055I0j~k~~>;#KqL}y+NLJUK4kTf3Y#;WiKh>hLxWd38iKb8uf$mh@7QY zTmHOZG%j)5ay%>!(qT8@HmDZiWQ=4^`Q;kF)$eEJO9H>u6+s9D=XKIn-8fgj9et3X z7e{zJXqtdO)j-Li8+sCkM&B^xQzl7m2ZMzi4{87g0S_%N1*zZ{YVF{kp6@!~7pAWR zSWC!WYn_tLHxhAhj;>@jkS_8hfxrmo;WNHVF&mnUC2q!cG5Mm|Bmh9$@C^|Zl=_wx zWsq25>C>{p3=$TGH40#O-5rd{QE41Qgvl`>FI%0qcL2|aIRI_n>zYM;=Oz@m2v!Cq z1ba*t5l}4u$+pq=A9msyYme`BWb?#!`R5E&a^+p~O?A^mXQ*6^4vOKc1N5JSRv@os zj*V?q69~^dw={KAO+Wg(R0dZ7V0!!u#_)|nF9%bTn!h11$?p}eVb1%;Aw?Ymn*YY( zSp*cr(JCxT_W)9_#NZ9ocEeWa>TCVwFQz5{1cd!YuviP9*IX$+zDz8XIEAl31JHlq zY_@L-q@d92V#qX%^1qSHK?cn6kcA{yQ3vHE1BCufN;dVx)Vp^Rmoa6pfL@9|Sg0iT zAGBJo8{X?$CBau5M`N9YTYSHV7>vN*WMdG-ryQpgF}X{r#~4T5j)YQm0i@|6HTTUq z{ul875^d+7rQN*VZhVpGApv9~WKabp;?mU6XQ`{%FKso!w7#QiuIeO4o3GF$%g0HT zcrE*j0}Kw{pK{2IK6^tf`{ckI{qE44#%61Y$6vPDzMgQR&G*JjPED6>;<|<<4YiZz zd>h+np0q!H!8Si(#x7O=wXI{E^QscxYN8)EkU%EIX*O*(l@XEqu5n}7KJ1_?!^i5% z5N~qO-&Il0*q0$WE_%cM0N*%Ah8TP|%aRxDn6NjKXL0JN(D;JtsT;u8!})POrePaG z?~R*XiUDkg=lsuw{_PGurR>_K!ppDEgf-BRX`-9F^P_>&Y8IO(II0M ztU&E)c8q?7rp{MDDYo_1m(+E@_AeNa1rg;nRWxQZ}qLd)wlXq V-|Aa^tMAKy{~rixz&`-k1OSIrE~5Yd literal 0 HcmV?d00001 diff --git a/utils/extract.go b/utils/extract.go new file mode 100644 index 0000000000..0559c6ce8e --- /dev/null +++ b/utils/extract.go @@ -0,0 +1,83 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" +) + +// ExtractTarGz takes in an io.Reader containing the bytes for a .tar.gz file and +// a destination string to extract to. A list of the file and directory names that +// were extracted is returned. +func ExtractTarGz(gzipStream io.Reader, dst string) ([]string, error) { + uncompressedStream, err := gzip.NewReader(gzipStream) + if err != nil { + return nil, fmt.Errorf("ExtractTarGz: NewReader failed: %s", err.Error()) + } + defer uncompressedStream.Close() + + tarReader := tar.NewReader(uncompressedStream) + + filenames := []string{} + + for true { + header, err := tarReader.Next() + + if err == io.EOF { + break + } + + if err != nil { + return nil, fmt.Errorf("ExtractTarGz: Next() failed: %s", err.Error()) + } + + switch header.Typeflag { + case tar.TypeDir: + if PathTraversesUpward(header.Name) { + return nil, fmt.Errorf("ExtractTarGz: path attempts to traverse upwards") + } + + path := filepath.Join(dst, header.Name) + if err := os.Mkdir(path, 0744); err != nil && !os.IsExist(err) { + return nil, fmt.Errorf("ExtractTarGz: Mkdir() failed: %s", err.Error()) + } + + filenames = append(filenames, header.Name) + case tar.TypeReg: + if PathTraversesUpward(header.Name) { + return nil, fmt.Errorf("ExtractTarGz: path attempts to traverse upwards") + } + + path := filepath.Join(dst, header.Name) + dir := filepath.Dir(path) + + if err := os.MkdirAll(dir, 0744); err != nil { + return nil, fmt.Errorf("ExtractTarGz: MkdirAll() failed: %s", err.Error()) + } + + outFile, err := os.Create(path) + if err != nil { + return nil, fmt.Errorf("ExtractTarGz: Create() failed: %s", err.Error()) + } + defer outFile.Close() + if _, err := io.Copy(outFile, tarReader); err != nil { + return nil, fmt.Errorf("ExtractTarGz: Copy() failed: %s", err.Error()) + } + + filenames = append(filenames, header.Name) + default: + return nil, fmt.Errorf( + "ExtractTarGz: unknown type: %v in %v", + header.Typeflag, + header.Name) + } + } + + return filenames, nil +} diff --git a/utils/path.go b/utils/path.go new file mode 100644 index 0000000000..5f921b41d3 --- /dev/null +++ b/utils/path.go @@ -0,0 +1,15 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "path/filepath" + "strings" +) + +// PathTraversesUpward will return true if the path attempts to traverse upwards by using +// ".." in the path. +func PathTraversesUpward(path string) bool { + return strings.HasPrefix(filepath.Clean(path), "..") +} diff --git a/utils/path_test.go b/utils/path_test.go new file mode 100644 index 0000000000..70b7c24fcb --- /dev/null +++ b/utils/path_test.go @@ -0,0 +1,31 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPathTraversesUpward(t *testing.T) { + cases := []struct { + input string + expected bool + }{ + {"../test/path", true}, + {"../../test/path", true}, + {"../../test/../path", true}, + {"test/../../path", true}, + {"test/path/../../", false}, + {"test", false}, + {"test/path", false}, + {"test/path/", false}, + {"test/path/file.ext", false}, + } + + for _, c := range cases { + assert.Equal(t, c.expected, PathTraversesUpward(c.input), c.input) + } +}