github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/app/plugin_install.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  // Installing a managed plugin consists of copying the uploaded plugin (*.tar.gz) to the filestore,
     5  // unpacking to the configured local directory (PluginSettings.Directory), and copying any webapp bundle therein
     6  // to the configured local client directory (PluginSettings.ClientDirectory). The unpacking and copy occurs
     7  // each time the server starts, ensuring it remains synchronized with the set of installed plugins.
     8  //
     9  // When a plugin is enabled, all connected websocket clients are notified so as to fetch any webapp bundle and
    10  // load the client-side portion of the plugin. This works well in a single-server system, but requires careful
    11  // coordination in a high-availability cluster with multiple servers. In particular, websocket clients must not be
    12  // notified of the newly enabled plugin until all servers in the cluster have finished unpacking the plugin, otherwise
    13  // the webapp bundle might not yet be available. Ideally, each server would just notify its own set of connected peers
    14  // after it finishes this process, but nothing prevents those clients from re-connecting to a different server behind
    15  // the load balancer that hasn't finished unpacking.
    16  //
    17  // To achieve this coordination, each server instead checks the status of its peers after unpacking. If it finds peers with
    18  // differing versions of the plugin, it skips the notification. If it finds all peers with the same version of the plugin,
    19  // it notifies all websocket clients connected to all peers. There's a small chance that this never occurs if the the last
    20  // server to finish unpacking dies before it can announce. There is also a chance that multiple servers decide to notify,
    21  // but the webapp handles this idempotently.
    22  //
    23  // Complicating this flow further are the various means of notifying. In addition to websocket events, there are cluster
    24  // messages between peers. There is a cluster message when the config changes and a plugin is enabled or disabled.
    25  // There is a cluster message when installing or uninstalling a plugin. There is a cluster message when peer's plugin change
    26  // its status. And finally the act of notifying websocket clients is propagated itself via a cluster message.
    27  //
    28  // The key methods involved in handling these notifications are notifyPluginEnabled and notifyPluginStatusesChanged.
    29  // Note that none of this complexity applies to single-server systems or to plugins without a webapp bundle.
    30  //
    31  // Finally, in addition to managed plugins, note that there are unmanaged and prepackaged plugins.
    32  // Unmanaged plugins are plugins installed manually to the configured local directory (PluginSettings.Directory).
    33  // Prepackaged plugins are included with the server. They otherwise follow the above flow, except do not get uploaded
    34  // to the filestore. Prepackaged plugins override all other plugins with the same plugin id, but only when the prepackaged
    35  // plugin is newer. Managed plugins unconditionally override unmanaged plugins with the same plugin id.
    36  //
    37  package app
    38  
    39  import (
    40  	"bytes"
    41  	"fmt"
    42  	"io"
    43  	"io/ioutil"
    44  	"net/http"
    45  	"os"
    46  	"path/filepath"
    47  
    48  	"github.com/blang/semver"
    49  	"github.com/pkg/errors"
    50  
    51  	"github.com/mattermost/mattermost-server/v5/mlog"
    52  	"github.com/mattermost/mattermost-server/v5/model"
    53  	"github.com/mattermost/mattermost-server/v5/services/filesstore"
    54  	"github.com/mattermost/mattermost-server/v5/utils"
    55  )
    56  
    57  // managedPluginFileName is the file name of the flag file that marks
    58  // a local plugin folder as "managed" by the file store.
    59  const managedPluginFileName = ".filestore"
    60  
    61  // fileStorePluginFolder is the folder name in the file store of the plugin bundles installed.
    62  const fileStorePluginFolder = "plugins"
    63  
    64  func (a *App) InstallPluginFromData(data model.PluginEventData) {
    65  	mlog.Debug("Installing plugin as per cluster message", mlog.String("plugin_id", data.Id))
    66  
    67  	pluginSignaturePathMap, appErr := a.getPluginsFromFolder()
    68  	if appErr != nil {
    69  		mlog.Error("Failed to get plugin signatures from filestore. Can't install plugin from data.", mlog.Err(appErr))
    70  		return
    71  	}
    72  	plugin, ok := pluginSignaturePathMap[data.Id]
    73  	if !ok {
    74  		mlog.Error("Failed to get plugin signature from filestore. Can't install plugin from data.", mlog.String("plugin id", data.Id))
    75  		return
    76  	}
    77  
    78  	reader, appErr := a.FileReader(plugin.path)
    79  	if appErr != nil {
    80  		mlog.Error("Failed to open plugin bundle from file store.", mlog.String("bundle", plugin.path), mlog.Err(appErr))
    81  		return
    82  	}
    83  	defer reader.Close()
    84  
    85  	var signature filesstore.ReadCloseSeeker
    86  	if *a.Config().PluginSettings.RequirePluginSignature {
    87  		signature, appErr = a.FileReader(plugin.signaturePath)
    88  		if appErr != nil {
    89  			mlog.Error("Failed to open plugin signature from file store.", mlog.Err(appErr))
    90  			return
    91  		}
    92  		defer signature.Close()
    93  	}
    94  
    95  	manifest, appErr := a.installPluginLocally(reader, signature, installPluginLocallyAlways)
    96  	if appErr != nil {
    97  		mlog.Error("Failed to sync plugin from file store", mlog.String("bundle", plugin.path), mlog.Err(appErr))
    98  		return
    99  	}
   100  
   101  	if err := a.notifyPluginEnabled(manifest); err != nil {
   102  		mlog.Error("Failed notify plugin enabled", mlog.Err(err))
   103  	}
   104  
   105  	if err := a.notifyPluginStatusesChanged(); err != nil {
   106  		mlog.Error("Failed to notify plugin status changed", mlog.Err(err))
   107  	}
   108  }
   109  
   110  func (a *App) RemovePluginFromData(data model.PluginEventData) {
   111  	mlog.Debug("Removing plugin as per cluster message", mlog.String("plugin_id", data.Id))
   112  
   113  	if err := a.removePluginLocally(data.Id); err != nil {
   114  		mlog.Error("Failed to remove plugin locally", mlog.Err(err), mlog.String("id", data.Id))
   115  	}
   116  
   117  	if err := a.notifyPluginStatusesChanged(); err != nil {
   118  		mlog.Error("failed to notify plugin status changed", mlog.Err(err))
   119  	}
   120  }
   121  
   122  // InstallPluginWithSignature verifies and installs plugin.
   123  func (a *App) InstallPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) {
   124  	return a.installPlugin(pluginFile, signature, installPluginLocallyAlways)
   125  }
   126  
   127  // InstallPlugin unpacks and installs a plugin but does not enable or activate it.
   128  func (a *App) InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) {
   129  	installationStrategy := installPluginLocallyOnlyIfNew
   130  	if replace {
   131  		installationStrategy = installPluginLocallyAlways
   132  	}
   133  
   134  	return a.installPlugin(pluginFile, nil, installationStrategy)
   135  }
   136  
   137  func (a *App) installPlugin(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
   138  	manifest, appErr := a.installPluginLocally(pluginFile, signature, installationStrategy)
   139  	if appErr != nil {
   140  		return nil, appErr
   141  	}
   142  
   143  	if signature != nil {
   144  		signature.Seek(0, 0)
   145  		if _, appErr = a.WriteFile(signature, a.getSignatureStorePath(manifest.Id)); appErr != nil {
   146  			return nil, model.NewAppError("saveSignature", "app.plugin.store_signature.app_error", nil, appErr.Error(), http.StatusInternalServerError)
   147  		}
   148  	}
   149  
   150  	// Store bundle in the file store to allow access from other servers.
   151  	pluginFile.Seek(0, 0)
   152  	if _, appErr := a.WriteFile(pluginFile, a.getBundleStorePath(manifest.Id)); appErr != nil {
   153  		return nil, model.NewAppError("uploadPlugin", "app.plugin.store_bundle.app_error", nil, appErr.Error(), http.StatusInternalServerError)
   154  	}
   155  
   156  	a.notifyClusterPluginEvent(
   157  		model.CLUSTER_EVENT_INSTALL_PLUGIN,
   158  		model.PluginEventData{
   159  			Id: manifest.Id,
   160  		},
   161  	)
   162  
   163  	if err := a.notifyPluginEnabled(manifest); err != nil {
   164  		mlog.Error("Failed notify plugin enabled", mlog.Err(err))
   165  	}
   166  
   167  	if err := a.notifyPluginStatusesChanged(); err != nil {
   168  		mlog.Error("Failed to notify plugin status changed", mlog.Err(err))
   169  	}
   170  
   171  	return manifest, nil
   172  }
   173  
   174  // InstallMarketplacePlugin installs a plugin listed in the marketplace server. It will get the plugin bundle
   175  // from the prepackaged folder, if available, or remotely if EnableRemoteMarketplace is true.
   176  func (a *App) InstallMarketplacePlugin(request *model.InstallMarketplacePluginRequest) (*model.Manifest, *model.AppError) {
   177  	var pluginFile, signatureFile io.ReadSeeker
   178  
   179  	prepackagedPlugin, appErr := a.getPrepackagedPlugin(request.Id, request.Version)
   180  	if appErr != nil && appErr.Id != "app.plugin.marketplace_plugins.not_found.app_error" {
   181  		return nil, appErr
   182  	}
   183  	if prepackagedPlugin != nil {
   184  		fileReader, err := os.Open(prepackagedPlugin.Path)
   185  		if err != nil {
   186  			err = errors.Wrapf(err, "failed to open prepackaged plugin %s", prepackagedPlugin.Path)
   187  			return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, err.Error(), http.StatusInternalServerError)
   188  		}
   189  		defer fileReader.Close()
   190  
   191  		pluginFile = fileReader
   192  		signatureFile = bytes.NewReader(prepackagedPlugin.Signature)
   193  	}
   194  
   195  	if *a.Config().PluginSettings.EnableRemoteMarketplace && pluginFile == nil {
   196  		var plugin *model.BaseMarketplacePlugin
   197  		plugin, appErr = a.getRemoteMarketplacePlugin(request.Id, request.Version)
   198  		if appErr != nil {
   199  			return nil, appErr
   200  		}
   201  
   202  		downloadedPluginBytes, err := a.DownloadFromURL(plugin.DownloadURL)
   203  		if err != nil {
   204  			return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.install_marketplace_plugin.app_error", nil, err.Error(), http.StatusInternalServerError)
   205  		}
   206  		signature, err := plugin.DecodeSignature()
   207  		if err != nil {
   208  			return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.signature_decode.app_error", nil, err.Error(), http.StatusNotImplemented)
   209  		}
   210  		pluginFile = bytes.NewReader(downloadedPluginBytes)
   211  		signatureFile = signature
   212  	}
   213  
   214  	if pluginFile == nil {
   215  		return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.not_found.app_error", nil, "", http.StatusInternalServerError)
   216  	}
   217  	if signatureFile == nil {
   218  		return nil, model.NewAppError("InstallMarketplacePlugin", "app.plugin.marketplace_plugins.signature_not_found.app_error", nil, "", http.StatusInternalServerError)
   219  	}
   220  
   221  	manifest, appErr := a.InstallPluginWithSignature(pluginFile, signatureFile)
   222  	if appErr != nil {
   223  		return nil, appErr
   224  	}
   225  
   226  	return manifest, nil
   227  }
   228  
   229  type pluginInstallationStrategy int
   230  
   231  const (
   232  	// installPluginLocallyOnlyIfNew installs the given plugin locally only if no plugin with the same id has been unpacked.
   233  	installPluginLocallyOnlyIfNew pluginInstallationStrategy = iota
   234  	// installPluginLocallyOnlyIfNewOrUpgrade installs the given plugin locally only if no plugin with the same id has been unpacked, or if such a plugin is older.
   235  	installPluginLocallyOnlyIfNewOrUpgrade
   236  	// installPluginLocallyAlways unconditionally installs the given plugin locally only, clobbering any existing plugin with the same id.
   237  	installPluginLocallyAlways
   238  )
   239  
   240  func (a *App) installPluginLocally(pluginFile, signature io.ReadSeeker, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
   241  	pluginsEnvironment := a.GetPluginsEnvironment()
   242  	if pluginsEnvironment == nil {
   243  		return nil, model.NewAppError("installPluginLocally", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
   244  	}
   245  
   246  	// verify signature
   247  	if signature != nil {
   248  		if err := a.VerifyPlugin(pluginFile, signature); err != nil {
   249  			return nil, err
   250  		}
   251  	}
   252  
   253  	tmpDir, err := ioutil.TempDir("", "plugintmp")
   254  	if err != nil {
   255  		return nil, model.NewAppError("installPluginLocally", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
   256  	}
   257  	defer os.RemoveAll(tmpDir)
   258  
   259  	manifest, pluginDir, appErr := extractPlugin(pluginFile, tmpDir)
   260  	if appErr != nil {
   261  		return nil, appErr
   262  	}
   263  
   264  	manifest, appErr = a.installExtractedPlugin(manifest, pluginDir, installationStrategy)
   265  	if appErr != nil {
   266  		return nil, appErr
   267  	}
   268  
   269  	return manifest, nil
   270  }
   271  
   272  func extractPlugin(pluginFile io.ReadSeeker, extractDir string) (*model.Manifest, string, *model.AppError) {
   273  	pluginFile.Seek(0, 0)
   274  	if err := extractTarGz(pluginFile, extractDir); err != nil {
   275  		return nil, "", model.NewAppError("extractPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
   276  	}
   277  
   278  	dir, err := ioutil.ReadDir(extractDir)
   279  	if err != nil {
   280  		return nil, "", model.NewAppError("extractPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
   281  	}
   282  
   283  	if len(dir) == 1 && dir[0].IsDir() {
   284  		extractDir = filepath.Join(extractDir, dir[0].Name())
   285  	}
   286  
   287  	manifest, _, err := model.FindManifest(extractDir)
   288  	if err != nil {
   289  		return nil, "", model.NewAppError("extractPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
   290  	}
   291  
   292  	if !model.IsValidPluginId(manifest.Id) {
   293  		return nil, "", model.NewAppError("installPluginLocally", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": model.MinIdLength, "Max": model.MaxIdLength, "Regex": model.ValidIdRegex}, "", http.StatusBadRequest)
   294  	}
   295  
   296  	return manifest, extractDir, nil
   297  }
   298  
   299  func (a *App) installExtractedPlugin(manifest *model.Manifest, fromPluginDir string, installationStrategy pluginInstallationStrategy) (*model.Manifest, *model.AppError) {
   300  	pluginsEnvironment := a.GetPluginsEnvironment()
   301  	if pluginsEnvironment == nil {
   302  		return nil, model.NewAppError("installExtractedPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
   303  	}
   304  
   305  	bundles, err := pluginsEnvironment.Available()
   306  	if err != nil {
   307  		return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
   308  	}
   309  
   310  	// Check for plugins installed with the same ID.
   311  	var existingManifest *model.Manifest
   312  	for _, bundle := range bundles {
   313  		if bundle.Manifest != nil && bundle.Manifest.Id == manifest.Id {
   314  			existingManifest = bundle.Manifest
   315  			break
   316  		}
   317  	}
   318  
   319  	if existingManifest != nil {
   320  		// Return an error if already installed and strategy disallows installation.
   321  		if installationStrategy == installPluginLocallyOnlyIfNew {
   322  			return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
   323  		}
   324  
   325  		// Skip installation if already installed and newer.
   326  		if installationStrategy == installPluginLocallyOnlyIfNewOrUpgrade {
   327  			var version, existingVersion semver.Version
   328  
   329  			version, err = semver.Parse(manifest.Version)
   330  			if err != nil {
   331  				return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
   332  			}
   333  
   334  			existingVersion, err = semver.Parse(existingManifest.Version)
   335  			if err != nil {
   336  				return nil, model.NewAppError("installExtractedPlugin", "app.plugin.invalid_version.app_error", nil, "", http.StatusBadRequest)
   337  			}
   338  
   339  			if version.LTE(existingVersion) {
   340  				mlog.Debug("Skipping local installation of plugin since existing version is newer", mlog.String("plugin_id", manifest.Id))
   341  				return nil, nil
   342  			}
   343  		}
   344  
   345  		// Otherwise remove the existing installation prior to install below.
   346  		mlog.Debug("Removing existing installation of plugin before local install", mlog.String("plugin_id", existingManifest.Id), mlog.String("version", existingManifest.Version))
   347  		if err := a.removePluginLocally(existingManifest.Id); err != nil {
   348  			return nil, model.NewAppError("installExtractedPlugin", "app.plugin.install_id_failed_remove.app_error", nil, "", http.StatusBadRequest)
   349  		}
   350  	}
   351  
   352  	pluginPath := filepath.Join(*a.Config().PluginSettings.Directory, manifest.Id)
   353  	err = utils.CopyDir(fromPluginDir, pluginPath)
   354  	if err != nil {
   355  		return nil, model.NewAppError("installExtractedPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
   356  	}
   357  
   358  	// Flag plugin locally as managed by the filestore.
   359  	f, err := os.Create(filepath.Join(pluginPath, managedPluginFileName))
   360  	if err != nil {
   361  		return nil, model.NewAppError("installExtractedPlugin", "app.plugin.flag_managed.app_error", nil, err.Error(), http.StatusInternalServerError)
   362  	}
   363  	f.Close()
   364  
   365  	if manifest.HasWebapp() {
   366  		updatedManifest, err := pluginsEnvironment.UnpackWebappBundle(manifest.Id)
   367  		if err != nil {
   368  			return nil, model.NewAppError("installExtractedPlugin", "app.plugin.webapp_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
   369  		}
   370  		manifest = updatedManifest
   371  	}
   372  
   373  	// Activate the plugin if enabled.
   374  	pluginState := a.Config().PluginSettings.PluginStates[manifest.Id]
   375  	if pluginState != nil && pluginState.Enable {
   376  		updatedManifest, _, err := pluginsEnvironment.Activate(manifest.Id)
   377  		if err != nil {
   378  			return nil, model.NewAppError("installExtractedPlugin", "app.plugin.restart.app_error", nil, err.Error(), http.StatusInternalServerError)
   379  		}
   380  		manifest = updatedManifest
   381  	}
   382  
   383  	return manifest, nil
   384  }
   385  
   386  func (a *App) RemovePlugin(id string) *model.AppError {
   387  	return a.removePlugin(id)
   388  }
   389  
   390  func (a *App) removePlugin(id string) *model.AppError {
   391  	// Disable plugin before removal to make sure this
   392  	// plugin remains disabled on re-install.
   393  	if err := a.DisablePlugin(id); err != nil {
   394  		return err
   395  	}
   396  
   397  	if err := a.removePluginLocally(id); err != nil {
   398  		return err
   399  	}
   400  
   401  	// Remove bundle from the file store.
   402  	storePluginFileName := a.getBundleStorePath(id)
   403  	bundleExist, err := a.FileExists(storePluginFileName)
   404  	if err != nil {
   405  		return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
   406  	}
   407  	if !bundleExist {
   408  		return nil
   409  	}
   410  	if err = a.RemoveFile(storePluginFileName); err != nil {
   411  		return model.NewAppError("removePlugin", "app.plugin.remove_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
   412  	}
   413  	if err = a.removeSignature(id); err != nil {
   414  		mlog.Error("Can't remove signature", mlog.Err(err))
   415  	}
   416  
   417  	a.notifyClusterPluginEvent(
   418  		model.CLUSTER_EVENT_REMOVE_PLUGIN,
   419  		model.PluginEventData{
   420  			Id: id,
   421  		},
   422  	)
   423  
   424  	if err := a.notifyPluginStatusesChanged(); err != nil {
   425  		mlog.Error("Failed to notify plugin status changed", mlog.Err(err))
   426  	}
   427  
   428  	return nil
   429  }
   430  
   431  func (a *App) removePluginLocally(id string) *model.AppError {
   432  	pluginsEnvironment := a.GetPluginsEnvironment()
   433  	if pluginsEnvironment == nil {
   434  		return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
   435  	}
   436  
   437  	plugins, err := pluginsEnvironment.Available()
   438  	if err != nil {
   439  		return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
   440  	}
   441  
   442  	var manifest *model.Manifest
   443  	var pluginPath string
   444  	for _, p := range plugins {
   445  		if p.Manifest != nil && p.Manifest.Id == id {
   446  			manifest = p.Manifest
   447  			pluginPath = filepath.Dir(p.ManifestPath)
   448  			break
   449  		}
   450  	}
   451  
   452  	if manifest == nil {
   453  		return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusNotFound)
   454  	}
   455  
   456  	pluginsEnvironment.Deactivate(id)
   457  	pluginsEnvironment.RemovePlugin(id)
   458  	a.UnregisterPluginCommands(id)
   459  
   460  	if err := os.RemoveAll(pluginPath); err != nil {
   461  		return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
   462  	}
   463  
   464  	return nil
   465  }
   466  
   467  func (a *App) removeSignature(pluginId string) *model.AppError {
   468  	filePath := a.getSignatureStorePath(pluginId)
   469  	exists, err := a.FileExists(filePath)
   470  	if err != nil {
   471  		return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
   472  	}
   473  	if !exists {
   474  		mlog.Debug("no plugin signature to remove", mlog.String("plugin_id", pluginId))
   475  		return nil
   476  	}
   477  	if err = a.RemoveFile(filePath); err != nil {
   478  		return model.NewAppError("removeSignature", "app.plugin.remove_bundle.app_error", nil, err.Error(), http.StatusInternalServerError)
   479  	}
   480  	return nil
   481  }
   482  
   483  func (a *App) getBundleStorePath(id string) string {
   484  	return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz", id))
   485  }
   486  
   487  func (a *App) getSignatureStorePath(id string) string {
   488  	return filepath.Join(fileStorePluginFolder, fmt.Sprintf("%s.tar.gz.sig", id))
   489  }