github.com/dschalla/mattermost-server@v4.8.1-rc1+incompatible/app/plugin.go (about) 1 // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package app 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/sha256" 10 "encoding/base64" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "net/http" 15 "os" 16 "path/filepath" 17 "strings" 18 "unicode/utf8" 19 20 l4g "github.com/alecthomas/log4go" 21 22 "github.com/gorilla/mux" 23 "github.com/mattermost/mattermost-server/model" 24 "github.com/mattermost/mattermost-server/utils" 25 26 builtinplugin "github.com/mattermost/mattermost-server/app/plugin" 27 "github.com/mattermost/mattermost-server/app/plugin/jira" 28 "github.com/mattermost/mattermost-server/app/plugin/ldapextras" 29 "github.com/mattermost/mattermost-server/app/plugin/zoom" 30 31 "github.com/mattermost/mattermost-server/plugin" 32 "github.com/mattermost/mattermost-server/plugin/pluginenv" 33 "github.com/mattermost/mattermost-server/plugin/rpcplugin" 34 "github.com/mattermost/mattermost-server/plugin/rpcplugin/sandbox" 35 ) 36 37 const ( 38 PLUGIN_MAX_ID_LENGTH = 190 39 ) 40 41 var prepackagedPlugins map[string]func(string) ([]byte, error) = map[string]func(string) ([]byte, error){ 42 "jira": jira.Asset, 43 "zoom": zoom.Asset, 44 } 45 46 func (a *App) initBuiltInPlugins() { 47 plugins := map[string]builtinplugin.Plugin{ 48 "ldapextras": &ldapextras.Plugin{}, 49 } 50 for id, p := range plugins { 51 l4g.Debug("Initializing built-in plugin: " + id) 52 api := &BuiltInPluginAPI{ 53 id: id, 54 router: a.Srv.Router.PathPrefix("/plugins/" + id).Subrouter(), 55 app: a, 56 } 57 p.Initialize(api) 58 } 59 a.AddConfigListener(func(before, after *model.Config) { 60 for _, p := range plugins { 61 p.OnConfigurationChange() 62 } 63 }) 64 for _, p := range plugins { 65 p.OnConfigurationChange() 66 } 67 } 68 69 // ActivatePlugins will activate any plugins enabled in the config 70 // and deactivate all other plugins. 71 func (a *App) ActivatePlugins() { 72 if a.PluginEnv == nil { 73 l4g.Error("plugin env not initialized") 74 return 75 } 76 77 plugins, err := a.PluginEnv.Plugins() 78 if err != nil { 79 l4g.Error("failed to activate plugins: " + err.Error()) 80 return 81 } 82 83 for _, plugin := range plugins { 84 id := plugin.Manifest.Id 85 86 pluginState := &model.PluginState{Enable: false} 87 if state, ok := a.Config().PluginSettings.PluginStates[id]; ok { 88 pluginState = state 89 } 90 91 active := a.PluginEnv.IsPluginActive(id) 92 93 if pluginState.Enable && !active { 94 if err := a.PluginEnv.ActivatePlugin(id); err != nil { 95 l4g.Error(err.Error()) 96 continue 97 } 98 99 if plugin.Manifest.HasClient() { 100 message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil) 101 message.Add("manifest", plugin.Manifest.ClientManifest()) 102 a.Publish(message) 103 } 104 105 l4g.Info("Activated %v plugin", id) 106 } else if !pluginState.Enable && active { 107 if err := a.deactivatePlugin(plugin.Manifest); err != nil { 108 l4g.Error(err.Error()) 109 } 110 } 111 } 112 } 113 114 func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError { 115 if err := a.PluginEnv.DeactivatePlugin(manifest.Id); err != nil { 116 return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest) 117 } 118 119 a.UnregisterPluginCommands(manifest.Id) 120 121 if manifest.HasClient() { 122 message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil) 123 message.Add("manifest", manifest.ClientManifest()) 124 a.Publish(message) 125 } 126 127 l4g.Info("Deactivated %v plugin", manifest.Id) 128 return nil 129 } 130 131 // InstallPlugin unpacks and installs a plugin but does not activate it. 132 func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) { 133 return a.installPlugin(pluginFile, false) 134 } 135 136 func (a *App) installPlugin(pluginFile io.Reader, allowPrepackaged bool) (*model.Manifest, *model.AppError) { 137 if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable { 138 return nil, model.NewAppError("installPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) 139 } 140 141 tmpDir, err := ioutil.TempDir("", "plugintmp") 142 if err != nil { 143 return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError) 144 } 145 defer os.RemoveAll(tmpDir) 146 147 if err := utils.ExtractTarGz(pluginFile, tmpDir); err != nil { 148 return nil, model.NewAppError("installPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest) 149 } 150 151 tmpPluginDir := tmpDir 152 dir, err := ioutil.ReadDir(tmpDir) 153 if err != nil { 154 return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError) 155 } 156 157 if len(dir) == 1 && dir[0].IsDir() { 158 tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name()) 159 } 160 161 manifest, _, err := model.FindManifest(tmpPluginDir) 162 if err != nil { 163 return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest) 164 } 165 166 if _, ok := prepackagedPlugins[manifest.Id]; ok && !allowPrepackaged { 167 return nil, model.NewAppError("installPlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest) 168 } 169 170 if utf8.RuneCountInString(manifest.Id) > PLUGIN_MAX_ID_LENGTH { 171 return nil, model.NewAppError("installPlugin", "app.plugin.id_length.app_error", map[string]interface{}{"Max": PLUGIN_MAX_ID_LENGTH}, err.Error(), http.StatusBadRequest) 172 } 173 174 bundles, err := a.PluginEnv.Plugins() 175 if err != nil { 176 return nil, model.NewAppError("installPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError) 177 } 178 179 for _, bundle := range bundles { 180 if bundle.Manifest.Id == manifest.Id { 181 return nil, model.NewAppError("installPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest) 182 } 183 } 184 185 err = utils.CopyDir(tmpPluginDir, filepath.Join(a.PluginEnv.SearchPath(), manifest.Id)) 186 if err != nil { 187 return nil, model.NewAppError("installPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError) 188 } 189 190 // Should add manifest validation and error handling here 191 192 return manifest, nil 193 } 194 195 func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) { 196 if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable { 197 return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) 198 } 199 200 plugins, err := a.PluginEnv.Plugins() 201 if err != nil { 202 return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError) 203 } 204 205 resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}} 206 for _, plugin := range plugins { 207 info := &model.PluginInfo{ 208 Manifest: *plugin.Manifest, 209 } 210 _, info.Prepackaged = prepackagedPlugins[plugin.Manifest.Id] 211 if a.PluginEnv.IsPluginActive(plugin.Manifest.Id) { 212 resp.Active = append(resp.Active, info) 213 } else { 214 resp.Inactive = append(resp.Inactive, info) 215 } 216 } 217 218 return resp, nil 219 } 220 221 func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) { 222 if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable { 223 return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) 224 } 225 226 plugins := a.PluginEnv.ActivePlugins() 227 228 manifests := make([]*model.Manifest, len(plugins)) 229 for i, plugin := range plugins { 230 manifests[i] = plugin.Manifest 231 } 232 233 return manifests, nil 234 } 235 236 func (a *App) RemovePlugin(id string) *model.AppError { 237 return a.removePlugin(id, false) 238 } 239 240 func (a *App) removePlugin(id string, allowPrepackaged bool) *model.AppError { 241 if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable { 242 return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) 243 } 244 245 if _, ok := prepackagedPlugins[id]; ok && !allowPrepackaged { 246 return model.NewAppError("removePlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest) 247 } 248 249 plugins, err := a.PluginEnv.Plugins() 250 if err != nil { 251 return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest) 252 } 253 254 var manifest *model.Manifest 255 for _, p := range plugins { 256 if p.Manifest.Id == id { 257 manifest = p.Manifest 258 break 259 } 260 } 261 262 if manifest == nil { 263 return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest) 264 } 265 266 if a.PluginEnv.IsPluginActive(id) { 267 err := a.deactivatePlugin(manifest) 268 if err != nil { 269 return err 270 } 271 } 272 273 err = os.RemoveAll(filepath.Join(a.PluginEnv.SearchPath(), id)) 274 if err != nil { 275 return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError) 276 } 277 278 return nil 279 } 280 281 // EnablePlugin will set the config for an installed plugin to enabled, triggering activation if inactive. 282 func (a *App) EnablePlugin(id string) *model.AppError { 283 if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable { 284 return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) 285 } 286 287 plugins, err := a.PluginEnv.Plugins() 288 if err != nil { 289 return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) 290 } 291 292 var manifest *model.Manifest 293 for _, p := range plugins { 294 if p.Manifest.Id == id { 295 manifest = p.Manifest 296 break 297 } 298 } 299 300 if manifest == nil { 301 return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest) 302 } 303 304 a.UpdateConfig(func(cfg *model.Config) { 305 cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true} 306 }) 307 308 if err := a.SaveConfig(a.Config(), true); err != nil { 309 return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) 310 } 311 312 return nil 313 } 314 315 // DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active. 316 func (a *App) DisablePlugin(id string) *model.AppError { 317 if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable { 318 return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) 319 } 320 321 plugins, err := a.PluginEnv.Plugins() 322 if err != nil { 323 return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) 324 } 325 326 var manifest *model.Manifest 327 for _, p := range plugins { 328 if p.Manifest.Id == id { 329 manifest = p.Manifest 330 break 331 } 332 } 333 334 if manifest == nil { 335 return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest) 336 } 337 338 a.UpdateConfig(func(cfg *model.Config) { 339 cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false} 340 }) 341 342 if err := a.SaveConfig(a.Config(), true); err != nil { 343 return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError) 344 } 345 346 return nil 347 } 348 349 func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride pluginenv.SupervisorProviderFunc) { 350 if !*a.Config().PluginSettings.Enable { 351 return 352 } 353 354 if a.PluginEnv != nil { 355 return 356 } 357 358 l4g.Info("Starting up plugins") 359 360 if err := os.Mkdir(pluginPath, 0744); err != nil && !os.IsExist(err) { 361 l4g.Error("failed to start up plugins: " + err.Error()) 362 return 363 } 364 365 if err := os.Mkdir(webappPath, 0744); err != nil && !os.IsExist(err) { 366 l4g.Error("failed to start up plugins: " + err.Error()) 367 return 368 } 369 370 options := []pluginenv.Option{ 371 pluginenv.SearchPath(pluginPath), 372 pluginenv.WebappPath(webappPath), 373 pluginenv.APIProvider(func(m *model.Manifest) (plugin.API, error) { 374 return &PluginAPI{ 375 id: m.Id, 376 app: a, 377 keyValueStore: &PluginKeyValueStore{ 378 id: m.Id, 379 app: a, 380 }, 381 }, nil 382 }), 383 } 384 385 if supervisorOverride != nil { 386 options = append(options, pluginenv.SupervisorProvider(supervisorOverride)) 387 } else if err := sandbox.CheckSupport(); err != nil { 388 l4g.Warn(err.Error()) 389 l4g.Warn("plugin sandboxing is not supported. plugins will run with the same access level as the server") 390 options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider)) 391 } else { 392 options = append(options, pluginenv.SupervisorProvider(sandbox.SupervisorProvider)) 393 } 394 395 if env, err := pluginenv.New(options...); err != nil { 396 l4g.Error("failed to start up plugins: " + err.Error()) 397 return 398 } else { 399 a.PluginEnv = env 400 } 401 402 for id, asset := range prepackagedPlugins { 403 if tarball, err := asset("plugin.tar.gz"); err != nil { 404 l4g.Error("failed to install prepackaged plugin: " + err.Error()) 405 } else if tarball != nil { 406 a.removePlugin(id, true) 407 if _, err := a.installPlugin(bytes.NewReader(tarball), true); err != nil { 408 l4g.Error("failed to install prepackaged plugin: " + err.Error()) 409 } 410 if _, ok := a.Config().PluginSettings.PluginStates[id]; !ok && id != "zoom" { 411 if err := a.EnablePlugin(id); err != nil { 412 l4g.Error("failed to enable prepackaged plugin: " + err.Error()) 413 } 414 } 415 } 416 } 417 418 a.RemoveConfigListener(a.PluginConfigListenerId) 419 a.PluginConfigListenerId = a.AddConfigListener(func(prevCfg, cfg *model.Config) { 420 if a.PluginEnv == nil { 421 return 422 } 423 424 if *prevCfg.PluginSettings.Enable && *cfg.PluginSettings.Enable { 425 a.ActivatePlugins() 426 } 427 428 for _, err := range a.PluginEnv.Hooks().OnConfigurationChange() { 429 l4g.Error(err.Error()) 430 } 431 }) 432 433 a.ActivatePlugins() 434 } 435 436 func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) { 437 if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable { 438 err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented) 439 l4g.Error(err.Error()) 440 w.WriteHeader(err.StatusCode) 441 w.Header().Set("Content-Type", "application/json") 442 w.Write([]byte(err.ToJson())) 443 return 444 } 445 446 a.servePluginRequest(w, r, a.PluginEnv.Hooks().ServeHTTP) 447 } 448 449 func (a *App) servePluginRequest(w http.ResponseWriter, r *http.Request, handler http.HandlerFunc) { 450 token := "" 451 452 authHeader := r.Header.Get(model.HEADER_AUTH) 453 if strings.HasPrefix(strings.ToUpper(authHeader), model.HEADER_BEARER+" ") { 454 token = authHeader[len(model.HEADER_BEARER)+1:] 455 } else if strings.HasPrefix(strings.ToLower(authHeader), model.HEADER_TOKEN+" ") { 456 token = authHeader[len(model.HEADER_TOKEN)+1:] 457 } else if cookie, _ := r.Cookie(model.SESSION_COOKIE_TOKEN); cookie != nil && (r.Method == "GET" || r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML) { 458 token = cookie.Value 459 } else { 460 token = r.URL.Query().Get("access_token") 461 } 462 463 r.Header.Del("Mattermost-User-Id") 464 if token != "" { 465 if session, err := a.GetSession(token); session != nil && err == nil { 466 r.Header.Set("Mattermost-User-Id", session.UserId) 467 } 468 } 469 470 cookies := r.Cookies() 471 r.Header.Del("Cookie") 472 for _, c := range cookies { 473 if c.Name != model.SESSION_COOKIE_TOKEN { 474 r.AddCookie(c) 475 } 476 } 477 r.Header.Del(model.HEADER_AUTH) 478 r.Header.Del("Referer") 479 480 params := mux.Vars(r) 481 482 newQuery := r.URL.Query() 483 newQuery.Del("access_token") 484 r.URL.RawQuery = newQuery.Encode() 485 r.URL.Path = strings.TrimPrefix(r.URL.Path, "/plugins/"+params["plugin_id"]) 486 487 handler(w, r.WithContext(context.WithValue(r.Context(), "plugin_id", params["plugin_id"]))) 488 } 489 490 func (a *App) ShutDownPlugins() { 491 if a.PluginEnv == nil { 492 return 493 } 494 495 l4g.Info("Shutting down plugins") 496 497 for _, err := range a.PluginEnv.Shutdown() { 498 l4g.Error(err.Error()) 499 } 500 a.RemoveConfigListener(a.PluginConfigListenerId) 501 a.PluginConfigListenerId = "" 502 a.PluginEnv = nil 503 } 504 505 func getKeyHash(key string) string { 506 hash := sha256.New() 507 hash.Write([]byte(key)) 508 return base64.StdEncoding.EncodeToString(hash.Sum(nil)) 509 } 510 511 func (a *App) SetPluginKey(pluginId string, key string, value []byte) *model.AppError { 512 kv := &model.PluginKeyValue{ 513 PluginId: pluginId, 514 Key: getKeyHash(key), 515 Value: value, 516 } 517 518 result := <-a.Srv.Store.Plugin().SaveOrUpdate(kv) 519 520 if result.Err != nil { 521 l4g.Error(result.Err.Error()) 522 } 523 524 return result.Err 525 } 526 527 func (a *App) GetPluginKey(pluginId string, key string) ([]byte, *model.AppError) { 528 result := <-a.Srv.Store.Plugin().Get(pluginId, getKeyHash(key)) 529 530 if result.Err != nil { 531 if result.Err.StatusCode == http.StatusNotFound { 532 return nil, nil 533 } 534 l4g.Error(result.Err.Error()) 535 return nil, result.Err 536 } 537 538 kv := result.Data.(*model.PluginKeyValue) 539 540 return kv.Value, nil 541 } 542 543 func (a *App) DeletePluginKey(pluginId string, key string) *model.AppError { 544 result := <-a.Srv.Store.Plugin().Delete(pluginId, getKeyHash(key)) 545 546 if result.Err != nil { 547 l4g.Error(result.Err.Error()) 548 } 549 550 return result.Err 551 } 552 553 type PluginCommand struct { 554 Command *model.Command 555 PluginId string 556 } 557 558 func (a *App) RegisterPluginCommand(pluginId string, command *model.Command) error { 559 if command.Trigger == "" { 560 return fmt.Errorf("invalid command") 561 } 562 563 command = &model.Command{ 564 Trigger: strings.ToLower(command.Trigger), 565 TeamId: command.TeamId, 566 AutoComplete: command.AutoComplete, 567 AutoCompleteDesc: command.AutoCompleteDesc, 568 AutoCompleteHint: command.AutoCompleteHint, 569 DisplayName: command.DisplayName, 570 } 571 572 a.pluginCommandsLock.Lock() 573 defer a.pluginCommandsLock.Unlock() 574 575 for _, pc := range a.pluginCommands { 576 if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId { 577 if pc.PluginId == pluginId { 578 pc.Command = command 579 return nil 580 } 581 } 582 } 583 584 a.pluginCommands = append(a.pluginCommands, &PluginCommand{ 585 Command: command, 586 PluginId: pluginId, 587 }) 588 return nil 589 } 590 591 func (a *App) UnregisterPluginCommand(pluginId, teamId, trigger string) { 592 trigger = strings.ToLower(trigger) 593 594 a.pluginCommandsLock.Lock() 595 defer a.pluginCommandsLock.Unlock() 596 597 var remaining []*PluginCommand 598 for _, pc := range a.pluginCommands { 599 if pc.Command.TeamId != teamId || pc.Command.Trigger != trigger { 600 remaining = append(remaining, pc) 601 } 602 } 603 a.pluginCommands = remaining 604 } 605 606 func (a *App) UnregisterPluginCommands(pluginId string) { 607 a.pluginCommandsLock.Lock() 608 defer a.pluginCommandsLock.Unlock() 609 610 var remaining []*PluginCommand 611 for _, pc := range a.pluginCommands { 612 if pc.PluginId != pluginId { 613 remaining = append(remaining, pc) 614 } 615 } 616 a.pluginCommands = remaining 617 } 618 619 func (a *App) PluginCommandsForTeam(teamId string) []*model.Command { 620 a.pluginCommandsLock.RLock() 621 defer a.pluginCommandsLock.RUnlock() 622 623 var commands []*model.Command 624 for _, pc := range a.pluginCommands { 625 if pc.Command.TeamId == "" || pc.Command.TeamId == teamId { 626 commands = append(commands, pc.Command) 627 } 628 } 629 return commands 630 } 631 632 func (a *App) ExecutePluginCommand(args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) { 633 parts := strings.Split(args.Command, " ") 634 trigger := parts[0][1:] 635 trigger = strings.ToLower(trigger) 636 637 a.pluginCommandsLock.RLock() 638 defer a.pluginCommandsLock.RUnlock() 639 640 for _, pc := range a.pluginCommands { 641 if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger { 642 response, appErr, err := a.PluginEnv.HooksForPlugin(pc.PluginId).ExecuteCommand(args) 643 if err != nil { 644 return pc.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) 645 } 646 return pc.Command, response, appErr 647 } 648 } 649 return nil, nil, nil 650 }