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 }