From 1e5c432e1029601a664454388ae366ef69618d62 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Mon, 25 Jun 2018 12:33:13 -0700 Subject: MM-10702 Moving plugins to use hashicorp go-plugin. (#8978) * Moving plugins to use hashicorp go-plugin. * Tweaks from feedback. --- plugin/api.go | 18 +- plugin/client.go | 51 ++ plugin/client_rpc.go | 331 +++++++ plugin/client_rpc_generated.go | 1103 +++++++++++++++++++++++ plugin/environment.go | 260 ++++++ plugin/example_hello_user_test.go | 39 - plugin/example_hello_world_test.go | 20 - plugin/example_test.go | 35 - plugin/hclog_adapter.go | 73 ++ plugin/hooks.go | 27 +- plugin/http.go | 91 ++ plugin/interface_generator/main.go | 377 ++++++++ plugin/io_rpc.go | 63 ++ plugin/plugin.go | 18 - plugin/pluginenv/environment.go | 396 -------- plugin/pluginenv/environment_test.go | 409 --------- plugin/pluginenv/options.go | 50 - plugin/pluginenv/options_test.go | 32 - plugin/pluginenv/search_path.go | 35 - plugin/pluginenv/search_path_test.go | 62 -- plugin/plugintest/api.go | 120 ++- plugin/plugintest/apioverride.go | 18 - plugin/plugintest/hooks.go | 40 +- plugin/plugintest/key_value_store.go | 70 -- plugin/rpcplugin/api.go | 718 --------------- plugin/rpcplugin/api_test.go | 300 ------ plugin/rpcplugin/hooks.go | 398 -------- plugin/rpcplugin/hooks_test.go | 237 ----- plugin/rpcplugin/http.go | 91 -- plugin/rpcplugin/http_test.go | 61 -- plugin/rpcplugin/io.go | 63 -- plugin/rpcplugin/ipc.go | 31 - plugin/rpcplugin/ipc_test.go | 63 -- plugin/rpcplugin/main.go | 47 - plugin/rpcplugin/main_test.go | 63 -- plugin/rpcplugin/muxer.go | 264 ------ plugin/rpcplugin/muxer_test.go | 197 ---- plugin/rpcplugin/process.go | 26 - plugin/rpcplugin/process_test.go | 60 -- plugin/rpcplugin/process_unix.go | 48 - plugin/rpcplugin/process_windows.go | 648 ------------- plugin/rpcplugin/rpcplugintest/rpcplugintest.go | 26 - plugin/rpcplugin/rpcplugintest/supervisor.go | 312 ------- plugin/rpcplugin/sandbox/main_test.go | 18 - plugin/rpcplugin/sandbox/sandbox.go | 34 - plugin/rpcplugin/sandbox/sandbox_linux.go | 488 ---------- plugin/rpcplugin/sandbox/sandbox_linux_test.go | 159 ---- plugin/rpcplugin/sandbox/sandbox_other.go | 22 - plugin/rpcplugin/sandbox/sandbox_test.go | 25 - plugin/rpcplugin/sandbox/seccomp_linux.go | 178 ---- plugin/rpcplugin/sandbox/seccomp_linux_amd64.go | 301 ------- plugin/rpcplugin/sandbox/seccomp_linux_other.go | 10 - plugin/rpcplugin/sandbox/seccomp_linux_test.go | 210 ----- plugin/rpcplugin/sandbox/supervisor.go | 33 - plugin/rpcplugin/sandbox/supervisor_test.go | 18 - plugin/rpcplugin/supervisor.go | 176 ---- plugin/rpcplugin/supervisor_test.go | 14 - plugin/supervisor.go | 102 ++- plugin/supervisor_test.go | 148 +++ 59 files changed, 2734 insertions(+), 6593 deletions(-) create mode 100644 plugin/client.go create mode 100644 plugin/client_rpc.go create mode 100644 plugin/client_rpc_generated.go create mode 100644 plugin/environment.go delete mode 100644 plugin/example_hello_user_test.go delete mode 100644 plugin/example_hello_world_test.go delete mode 100644 plugin/example_test.go create mode 100644 plugin/hclog_adapter.go create mode 100644 plugin/http.go create mode 100644 plugin/interface_generator/main.go create mode 100644 plugin/io_rpc.go delete mode 100644 plugin/plugin.go delete mode 100644 plugin/pluginenv/environment.go delete mode 100644 plugin/pluginenv/environment_test.go delete mode 100644 plugin/pluginenv/options.go delete mode 100644 plugin/pluginenv/options_test.go delete mode 100644 plugin/pluginenv/search_path.go delete mode 100644 plugin/pluginenv/search_path_test.go delete mode 100644 plugin/plugintest/apioverride.go delete mode 100644 plugin/plugintest/key_value_store.go delete mode 100644 plugin/rpcplugin/api.go delete mode 100644 plugin/rpcplugin/api_test.go delete mode 100644 plugin/rpcplugin/hooks.go delete mode 100644 plugin/rpcplugin/hooks_test.go delete mode 100644 plugin/rpcplugin/http.go delete mode 100644 plugin/rpcplugin/http_test.go delete mode 100644 plugin/rpcplugin/io.go delete mode 100644 plugin/rpcplugin/ipc.go delete mode 100644 plugin/rpcplugin/ipc_test.go delete mode 100644 plugin/rpcplugin/main.go delete mode 100644 plugin/rpcplugin/main_test.go delete mode 100644 plugin/rpcplugin/muxer.go delete mode 100644 plugin/rpcplugin/muxer_test.go delete mode 100644 plugin/rpcplugin/process.go delete mode 100644 plugin/rpcplugin/process_test.go delete mode 100644 plugin/rpcplugin/process_unix.go delete mode 100644 plugin/rpcplugin/process_windows.go delete mode 100644 plugin/rpcplugin/rpcplugintest/rpcplugintest.go delete mode 100644 plugin/rpcplugin/rpcplugintest/supervisor.go delete mode 100644 plugin/rpcplugin/sandbox/main_test.go delete mode 100644 plugin/rpcplugin/sandbox/sandbox.go delete mode 100644 plugin/rpcplugin/sandbox/sandbox_linux.go delete mode 100644 plugin/rpcplugin/sandbox/sandbox_linux_test.go delete mode 100644 plugin/rpcplugin/sandbox/sandbox_other.go delete mode 100644 plugin/rpcplugin/sandbox/sandbox_test.go delete mode 100644 plugin/rpcplugin/sandbox/seccomp_linux.go delete mode 100644 plugin/rpcplugin/sandbox/seccomp_linux_amd64.go delete mode 100644 plugin/rpcplugin/sandbox/seccomp_linux_other.go delete mode 100644 plugin/rpcplugin/sandbox/seccomp_linux_test.go delete mode 100644 plugin/rpcplugin/sandbox/supervisor.go delete mode 100644 plugin/rpcplugin/sandbox/supervisor_test.go delete mode 100644 plugin/rpcplugin/supervisor.go delete mode 100644 plugin/rpcplugin/supervisor_test.go create mode 100644 plugin/supervisor_test.go (limited to 'plugin') diff --git a/plugin/api.go b/plugin/api.go index d62c2f069..ed2bfa733 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -4,6 +4,7 @@ package plugin import ( + "github.com/hashicorp/go-plugin" "github.com/mattermost/mattermost-server/model" ) @@ -104,17 +105,18 @@ type API interface { // UpdatePost updates a post. UpdatePost(post *model.Post) (*model.Post, *model.AppError) - // KeyValueStore returns an object for accessing the persistent key value storage. - KeyValueStore() KeyValueStore -} - -type KeyValueStore interface { // Set will store a key-value pair, unique per plugin. - Set(key string, value []byte) *model.AppError + KVSet(key string, value []byte) *model.AppError // Get will retrieve a value based on the key. Returns nil for non-existent keys. - Get(key string) ([]byte, *model.AppError) + KVGet(key string) ([]byte, *model.AppError) // Delete will remove a key-value pair. Returns nil for non-existent keys. - Delete(key string) *model.AppError + KVDelete(key string) *model.AppError +} + +var Handshake = plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "MATTERMOST_PLUGIN", + MagicCookieValue: "Securely message teams, anywhere.", } diff --git a/plugin/client.go b/plugin/client.go new file mode 100644 index 000000000..3f6fbc7a6 --- /dev/null +++ b/plugin/client.go @@ -0,0 +1,51 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin + +import ( + "github.com/hashicorp/go-plugin" +) + +// Starts the serving of a Mattermost plugin over rpc or gRPC +// Call this when your plugin is ready to start +func ClientMain(pluginImplementation interface{}) { + if impl, ok := pluginImplementation.(interface { + SetAPI(api API) + SetSelfRef(ref interface{}) + }); !ok { + panic("Plugin implementation given must embed plugin.MattermostPlugin") + } else { + impl.SetAPI(nil) + impl.SetSelfRef(pluginImplementation) + } + + pluginMap := map[string]plugin.Plugin{ + "hooks": &HooksPlugin{hooks: pluginImplementation}, + } + + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: Handshake, + Plugins: pluginMap, + }) +} + +type MattermostPlugin struct { + API API + selfRef interface{} // This is so we can unmarshal into our parent +} + +func (p *MattermostPlugin) SetAPI(api API) { + p.API = api +} + +func (p *MattermostPlugin) SetSelfRef(ref interface{}) { + p.selfRef = ref +} + +func (p *MattermostPlugin) OnConfigurationChange() error { + if p.selfRef != nil { + return p.API.LoadPluginConfiguration(p.selfRef) + } + return nil +} diff --git a/plugin/client_rpc.go b/plugin/client_rpc.go new file mode 100644 index 000000000..159d41201 --- /dev/null +++ b/plugin/client_rpc.go @@ -0,0 +1,331 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +//go:generate go run interface_generator/main.go + +package plugin + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "io/ioutil" + "net/http" + "net/rpc" + "reflect" + + "github.com/hashicorp/go-plugin" + "github.com/mattermost/mattermost-server/mlog" + "github.com/mattermost/mattermost-server/model" +) + +var HookNameToId map[string]int = make(map[string]int) + +type HooksRPCClient struct { + client *rpc.Client + log *mlog.Logger + muxBroker *plugin.MuxBroker + apiImpl API + implemented [TotalHooksId]bool +} + +type HooksRPCServer struct { + impl interface{} + muxBroker *plugin.MuxBroker + apiRPCClient *APIRPCClient + log *mlog.Logger +} + +// Implements hashicorp/go-plugin/plugin.Plugin interface to connect the hooks of a plugin +type HooksPlugin struct { + hooks interface{} + apiImpl API + log *mlog.Logger +} + +func (p *HooksPlugin) Server(b *plugin.MuxBroker) (interface{}, error) { + return &HooksRPCServer{impl: p.hooks, muxBroker: b}, nil +} + +func (p *HooksPlugin) Client(b *plugin.MuxBroker, client *rpc.Client) (interface{}, error) { + return &HooksRPCClient{client: client, log: p.log, muxBroker: b, apiImpl: p.apiImpl}, nil +} + +type APIRPCClient struct { + client *rpc.Client + log *mlog.Logger +} + +type APIRPCServer struct { + impl API +} + +// Registering some types used by MM for encoding/gob used by rpc +func init() { + gob.Register([]*model.SlackAttachment{}) + gob.Register([]interface{}{}) + gob.Register(map[string]interface{}{}) +} + +// These enforce compile time checks to make sure types implement the interface +// If you are getting an error here, you probably need to run `make pluginapi` to +// autogenerate RPC glue code +var _ plugin.Plugin = &HooksPlugin{} +var _ Hooks = &HooksRPCClient{} + +// +// Below are specal cases for hooks or APIs that can not be auto generated +// + +func (g *HooksRPCClient) Implemented() (impl []string, err error) { + err = g.client.Call("Plugin.Implemented", struct{}{}, &impl) + for _, hookName := range impl { + if hookId, ok := HookNameToId[hookName]; ok { + g.implemented[hookId] = true + } + } + return +} + +// Implemented replies with the names of the hooks that are implemented. +func (s *HooksRPCServer) Implemented(args struct{}, reply *[]string) error { + ifaceType := reflect.TypeOf((*Hooks)(nil)).Elem() + implType := reflect.TypeOf(s.impl) + selfType := reflect.TypeOf(s) + var methods []string + for i := 0; i < ifaceType.NumMethod(); i++ { + method := ifaceType.Method(i) + if m, ok := implType.MethodByName(method.Name); !ok { + continue + } else if m.Type.NumIn() != method.Type.NumIn()+1 { + continue + } else if m.Type.NumOut() != method.Type.NumOut() { + continue + } else { + match := true + for j := 0; j < method.Type.NumIn(); j++ { + if m.Type.In(j+1) != method.Type.In(j) { + match = false + break + } + } + for j := 0; j < method.Type.NumOut(); j++ { + if m.Type.Out(j) != method.Type.Out(j) { + match = false + break + } + } + if !match { + continue + } + } + if _, ok := selfType.MethodByName(method.Name); !ok { + continue + } + methods = append(methods, method.Name) + } + *reply = methods + return nil +} + +type OnActivateArgs struct { + APIMuxId uint32 +} + +type OnActivateReturns struct { + A error +} + +func (g *HooksRPCClient) OnActivate() error { + muxId := g.muxBroker.NextId() + go g.muxBroker.AcceptAndServe(muxId, &APIRPCServer{ + impl: g.apiImpl, + }) + + _args := &OnActivateArgs{ + APIMuxId: muxId, + } + _returns := &OnActivateReturns{} + + if err := g.client.Call("Plugin.OnActivate", _args, _returns); err != nil { + g.log.Error("RPC call to OnActivate plugin failed.", mlog.Err(err)) + } + return _returns.A +} + +func (s *HooksRPCServer) OnActivate(args *OnActivateArgs, returns *OnActivateReturns) error { + connection, err := s.muxBroker.Dial(args.APIMuxId) + if err != nil { + return err // Where does this go? + } + + // Settings for this should come from the parent process, for now just set it up + // though stdout. + logger := mlog.NewLogger(&mlog.LoggerConfiguration{ + EnableConsole: true, + ConsoleJson: true, + ConsoleLevel: mlog.LevelDebug, + EnableFile: false, + }) + logger = logger.With(mlog.Bool("plugin_subprocess", true)) + + s.log = logger + + s.apiRPCClient = &APIRPCClient{ + client: rpc.NewClient(connection), + log: logger, + } + + if mmplugin, ok := s.impl.(interface { + SetAPI(api API) + OnConfigurationChange() error + }); !ok { + } else { + mmplugin.SetAPI(s.apiRPCClient) + mmplugin.OnConfigurationChange() + } + + if hook, ok := s.impl.(interface { + OnActivate() error + }); ok { + returns.A = hook.OnActivate() + } + return nil +} + +type LoadPluginConfigurationArgs struct { +} + +type LoadPluginConfigurationReturns struct { + A []byte +} + +func (g *APIRPCClient) LoadPluginConfiguration(dest interface{}) error { + _args := &LoadPluginConfigurationArgs{} + _returns := &LoadPluginConfigurationReturns{} + if err := g.client.Call("Plugin.LoadPluginConfiguration", _args, _returns); err != nil { + g.log.Error("RPC call to LoadPluginConfiguration API failed.", mlog.Err(err)) + } + return json.Unmarshal(_returns.A, dest) +} + +func (s *APIRPCServer) LoadPluginConfiguration(args *LoadPluginConfigurationArgs, returns *LoadPluginConfigurationReturns) error { + var config interface{} + if hook, ok := s.impl.(interface { + LoadPluginConfiguration(dest interface{}) error + }); ok { + if err := hook.LoadPluginConfiguration(&config); err != nil { + return err + } + } + b, err := json.Marshal(config) + if err != nil { + return err + } + returns.A = b + return nil +} + +func init() { + HookNameToId["ServeHTTP"] = ServeHTTPId +} + +type ServeHTTPArgs struct { + ResponseWriterStream uint32 + Request *http.Request + RequestBodyStream uint32 +} + +func (g *HooksRPCClient) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !g.implemented[ServeHTTPId] { + http.NotFound(w, r) + return + } + + serveHTTPStreamId := g.muxBroker.NextId() + go func() { + connection, err := g.muxBroker.Accept(serveHTTPStreamId) + if err != nil { + g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't accept connection", mlog.Uint32("serve_http_stream_id", serveHTTPStreamId), mlog.Err(err)) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + return + } + defer connection.Close() + + rpcServer := rpc.NewServer() + if err := rpcServer.RegisterName("Plugin", &HTTPResponseWriterRPCServer{w: w}); err != nil { + g.log.Error("Plugin failed to ServeHTTP, coulden't register RPC name", mlog.Err(err)) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + return + } + rpcServer.ServeConn(connection) + }() + + requestBodyStreamId := uint32(0) + if r.Body != nil { + requestBodyStreamId = g.muxBroker.NextId() + go func() { + bodyConnection, err := g.muxBroker.Accept(requestBodyStreamId) + if err != nil { + g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't Accept request body connecion", mlog.Err(err)) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + return + } + defer bodyConnection.Close() + ServeIOReader(r.Body, bodyConnection) + }() + } + + forwardedRequest := &http.Request{ + Method: r.Method, + URL: r.URL, + Proto: r.Proto, + ProtoMajor: r.ProtoMajor, + ProtoMinor: r.ProtoMinor, + Header: r.Header, + Host: r.Host, + RemoteAddr: r.RemoteAddr, + RequestURI: r.RequestURI, + } + + if err := g.client.Call("Plugin.ServeHTTP", ServeHTTPArgs{ + ResponseWriterStream: serveHTTPStreamId, + Request: forwardedRequest, + RequestBodyStream: requestBodyStreamId, + }, nil); err != nil { + mlog.Error("Plugin failed to ServeHTTP, RPC call failed", mlog.Err(err)) + http.Error(w, "500 internal server error", http.StatusInternalServerError) + } + return +} + +func (s *HooksRPCServer) ServeHTTP(args *ServeHTTPArgs, returns *struct{}) error { + connection, err := s.muxBroker.Dial(args.ResponseWriterStream) + if err != nil { + s.log.Debug("Can't connect to remote response writer stream", mlog.Err(err)) + return err + } + w := ConnectHTTPResponseWriter(connection) + defer w.Close() + + r := args.Request + if args.RequestBodyStream != 0 { + connection, err := s.muxBroker.Dial(args.RequestBodyStream) + if err != nil { + s.log.Debug("Can't connect to remote response writer stream", mlog.Err(err)) + return err + } + r.Body = ConnectIOReader(connection) + } else { + r.Body = ioutil.NopCloser(&bytes.Buffer{}) + } + defer r.Body.Close() + + if hook, ok := s.impl.(http.Handler); ok { + hook.ServeHTTP(w, r) + } else { + http.NotFound(w, r) + } + + return nil +} diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go new file mode 100644 index 000000000..b32a3e36e --- /dev/null +++ b/plugin/client_rpc_generated.go @@ -0,0 +1,1103 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// Code generated by "make pluginapi" +// DO NOT EDIT + +package plugin + +import ( + "github.com/mattermost/mattermost-server/mlog" + "github.com/mattermost/mattermost-server/model" +) + +func init() { + HookNameToId["OnDeactivate"] = OnDeactivateId +} + +type OnDeactivateArgs struct { +} + +type OnDeactivateReturns struct { + A error +} + +func (g *HooksRPCClient) OnDeactivate() error { + _args := &OnDeactivateArgs{} + _returns := &OnDeactivateReturns{} + if g.implemented[OnDeactivateId] { + if err := g.client.Call("Plugin.OnDeactivate", _args, _returns); err != nil { + g.log.Error("RPC call OnDeactivate to plugin failed.", mlog.Err(err)) + } + } + return _returns.A +} + +func (s *HooksRPCServer) OnDeactivate(args *OnDeactivateArgs, returns *OnDeactivateReturns) error { + if hook, ok := s.impl.(interface { + OnDeactivate() error + }); ok { + returns.A = hook.OnDeactivate() + } + return nil +} + +func init() { + HookNameToId["OnConfigurationChange"] = OnConfigurationChangeId +} + +type OnConfigurationChangeArgs struct { +} + +type OnConfigurationChangeReturns struct { + A error +} + +func (g *HooksRPCClient) OnConfigurationChange() error { + _args := &OnConfigurationChangeArgs{} + _returns := &OnConfigurationChangeReturns{} + if g.implemented[OnConfigurationChangeId] { + if err := g.client.Call("Plugin.OnConfigurationChange", _args, _returns); err != nil { + g.log.Error("RPC call OnConfigurationChange to plugin failed.", mlog.Err(err)) + } + } + return _returns.A +} + +func (s *HooksRPCServer) OnConfigurationChange(args *OnConfigurationChangeArgs, returns *OnConfigurationChangeReturns) error { + if hook, ok := s.impl.(interface { + OnConfigurationChange() error + }); ok { + returns.A = hook.OnConfigurationChange() + } + return nil +} + +func init() { + HookNameToId["ExecuteCommand"] = ExecuteCommandId +} + +type ExecuteCommandArgs struct { + A *model.CommandArgs +} + +type ExecuteCommandReturns struct { + A *model.CommandResponse + B *model.AppError +} + +func (g *HooksRPCClient) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { + _args := &ExecuteCommandArgs{args} + _returns := &ExecuteCommandReturns{} + if g.implemented[ExecuteCommandId] { + if err := g.client.Call("Plugin.ExecuteCommand", _args, _returns); err != nil { + g.log.Error("RPC call ExecuteCommand to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *HooksRPCServer) ExecuteCommand(args *ExecuteCommandArgs, returns *ExecuteCommandReturns) error { + if hook, ok := s.impl.(interface { + ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) + }); ok { + returns.A, returns.B = hook.ExecuteCommand(args.A) + } + return nil +} + +func init() { + HookNameToId["MessageWillBePosted"] = MessageWillBePostedId +} + +type MessageWillBePostedArgs struct { + A *model.Post +} + +type MessageWillBePostedReturns struct { + A *model.Post + B string +} + +func (g *HooksRPCClient) MessageWillBePosted(post *model.Post) (*model.Post, string) { + _args := &MessageWillBePostedArgs{post} + _returns := &MessageWillBePostedReturns{} + if g.implemented[MessageWillBePostedId] { + if err := g.client.Call("Plugin.MessageWillBePosted", _args, _returns); err != nil { + g.log.Error("RPC call MessageWillBePosted to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *HooksRPCServer) MessageWillBePosted(args *MessageWillBePostedArgs, returns *MessageWillBePostedReturns) error { + if hook, ok := s.impl.(interface { + MessageWillBePosted(post *model.Post) (*model.Post, string) + }); ok { + returns.A, returns.B = hook.MessageWillBePosted(args.A) + } + return nil +} + +func init() { + HookNameToId["MessageWillBeUpdated"] = MessageWillBeUpdatedId +} + +type MessageWillBeUpdatedArgs struct { + A *model.Post + B *model.Post +} + +type MessageWillBeUpdatedReturns struct { + A *model.Post + B string +} + +func (g *HooksRPCClient) MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string) { + _args := &MessageWillBeUpdatedArgs{newPost, oldPost} + _returns := &MessageWillBeUpdatedReturns{} + if g.implemented[MessageWillBeUpdatedId] { + if err := g.client.Call("Plugin.MessageWillBeUpdated", _args, _returns); err != nil { + g.log.Error("RPC call MessageWillBeUpdated to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *HooksRPCServer) MessageWillBeUpdated(args *MessageWillBeUpdatedArgs, returns *MessageWillBeUpdatedReturns) error { + if hook, ok := s.impl.(interface { + MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string) + }); ok { + returns.A, returns.B = hook.MessageWillBeUpdated(args.A, args.B) + } + return nil +} + +func init() { + HookNameToId["MessageHasBeenPosted"] = MessageHasBeenPostedId +} + +type MessageHasBeenPostedArgs struct { + A *model.Post +} + +type MessageHasBeenPostedReturns struct { +} + +func (g *HooksRPCClient) MessageHasBeenPosted(post *model.Post) { + _args := &MessageHasBeenPostedArgs{post} + _returns := &MessageHasBeenPostedReturns{} + if g.implemented[MessageHasBeenPostedId] { + if err := g.client.Call("Plugin.MessageHasBeenPosted", _args, _returns); err != nil { + g.log.Error("RPC call MessageHasBeenPosted to plugin failed.", mlog.Err(err)) + } + } + return +} + +func (s *HooksRPCServer) MessageHasBeenPosted(args *MessageHasBeenPostedArgs, returns *MessageHasBeenPostedReturns) error { + if hook, ok := s.impl.(interface { + MessageHasBeenPosted(post *model.Post) + }); ok { + hook.MessageHasBeenPosted(args.A) + } + return nil +} + +func init() { + HookNameToId["MessageHasBeenUpdated"] = MessageHasBeenUpdatedId +} + +type MessageHasBeenUpdatedArgs struct { + A *model.Post + B *model.Post +} + +type MessageHasBeenUpdatedReturns struct { +} + +func (g *HooksRPCClient) MessageHasBeenUpdated(newPost, oldPost *model.Post) { + _args := &MessageHasBeenUpdatedArgs{newPost, oldPost} + _returns := &MessageHasBeenUpdatedReturns{} + if g.implemented[MessageHasBeenUpdatedId] { + if err := g.client.Call("Plugin.MessageHasBeenUpdated", _args, _returns); err != nil { + g.log.Error("RPC call MessageHasBeenUpdated to plugin failed.", mlog.Err(err)) + } + } + return +} + +func (s *HooksRPCServer) MessageHasBeenUpdated(args *MessageHasBeenUpdatedArgs, returns *MessageHasBeenUpdatedReturns) error { + if hook, ok := s.impl.(interface { + MessageHasBeenUpdated(newPost, oldPost *model.Post) + }); ok { + hook.MessageHasBeenUpdated(args.A, args.B) + } + return nil +} + +type RegisterCommandArgs struct { + A *model.Command +} + +type RegisterCommandReturns struct { + A error +} + +func (g *APIRPCClient) RegisterCommand(command *model.Command) error { + _args := &RegisterCommandArgs{command} + _returns := &RegisterCommandReturns{} + if err := g.client.Call("Plugin.RegisterCommand", _args, _returns); err != nil { + g.log.Error("RPC call to RegisterCommand API failed.", mlog.Err(err)) + } + return _returns.A +} + +func (s *APIRPCServer) RegisterCommand(args *RegisterCommandArgs, returns *RegisterCommandReturns) error { + if hook, ok := s.impl.(interface { + RegisterCommand(command *model.Command) error + }); ok { + returns.A = hook.RegisterCommand(args.A) + } + return nil +} + +type UnregisterCommandArgs struct { + A string + B string +} + +type UnregisterCommandReturns struct { + A error +} + +func (g *APIRPCClient) UnregisterCommand(teamId, trigger string) error { + _args := &UnregisterCommandArgs{teamId, trigger} + _returns := &UnregisterCommandReturns{} + if err := g.client.Call("Plugin.UnregisterCommand", _args, _returns); err != nil { + g.log.Error("RPC call to UnregisterCommand API failed.", mlog.Err(err)) + } + return _returns.A +} + +func (s *APIRPCServer) UnregisterCommand(args *UnregisterCommandArgs, returns *UnregisterCommandReturns) error { + if hook, ok := s.impl.(interface { + UnregisterCommand(teamId, trigger string) error + }); ok { + returns.A = hook.UnregisterCommand(args.A, args.B) + } + return nil +} + +type CreateUserArgs struct { + A *model.User +} + +type CreateUserReturns struct { + A *model.User + B *model.AppError +} + +func (g *APIRPCClient) CreateUser(user *model.User) (*model.User, *model.AppError) { + _args := &CreateUserArgs{user} + _returns := &CreateUserReturns{} + if err := g.client.Call("Plugin.CreateUser", _args, _returns); err != nil { + g.log.Error("RPC call to CreateUser API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) CreateUser(args *CreateUserArgs, returns *CreateUserReturns) error { + if hook, ok := s.impl.(interface { + CreateUser(user *model.User) (*model.User, *model.AppError) + }); ok { + returns.A, returns.B = hook.CreateUser(args.A) + } + return nil +} + +type DeleteUserArgs struct { + A string +} + +type DeleteUserReturns struct { + A *model.AppError +} + +func (g *APIRPCClient) DeleteUser(userId string) *model.AppError { + _args := &DeleteUserArgs{userId} + _returns := &DeleteUserReturns{} + if err := g.client.Call("Plugin.DeleteUser", _args, _returns); err != nil { + g.log.Error("RPC call to DeleteUser API failed.", mlog.Err(err)) + } + return _returns.A +} + +func (s *APIRPCServer) DeleteUser(args *DeleteUserArgs, returns *DeleteUserReturns) error { + if hook, ok := s.impl.(interface { + DeleteUser(userId string) *model.AppError + }); ok { + returns.A = hook.DeleteUser(args.A) + } + return nil +} + +type GetUserArgs struct { + A string +} + +type GetUserReturns struct { + A *model.User + B *model.AppError +} + +func (g *APIRPCClient) GetUser(userId string) (*model.User, *model.AppError) { + _args := &GetUserArgs{userId} + _returns := &GetUserReturns{} + if err := g.client.Call("Plugin.GetUser", _args, _returns); err != nil { + g.log.Error("RPC call to GetUser API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetUser(args *GetUserArgs, returns *GetUserReturns) error { + if hook, ok := s.impl.(interface { + GetUser(userId string) (*model.User, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetUser(args.A) + } + return nil +} + +type GetUserByEmailArgs struct { + A string +} + +type GetUserByEmailReturns struct { + A *model.User + B *model.AppError +} + +func (g *APIRPCClient) GetUserByEmail(email string) (*model.User, *model.AppError) { + _args := &GetUserByEmailArgs{email} + _returns := &GetUserByEmailReturns{} + if err := g.client.Call("Plugin.GetUserByEmail", _args, _returns); err != nil { + g.log.Error("RPC call to GetUserByEmail API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetUserByEmail(args *GetUserByEmailArgs, returns *GetUserByEmailReturns) error { + if hook, ok := s.impl.(interface { + GetUserByEmail(email string) (*model.User, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetUserByEmail(args.A) + } + return nil +} + +type GetUserByUsernameArgs struct { + A string +} + +type GetUserByUsernameReturns struct { + A *model.User + B *model.AppError +} + +func (g *APIRPCClient) GetUserByUsername(name string) (*model.User, *model.AppError) { + _args := &GetUserByUsernameArgs{name} + _returns := &GetUserByUsernameReturns{} + if err := g.client.Call("Plugin.GetUserByUsername", _args, _returns); err != nil { + g.log.Error("RPC call to GetUserByUsername API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetUserByUsername(args *GetUserByUsernameArgs, returns *GetUserByUsernameReturns) error { + if hook, ok := s.impl.(interface { + GetUserByUsername(name string) (*model.User, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetUserByUsername(args.A) + } + return nil +} + +type UpdateUserArgs struct { + A *model.User +} + +type UpdateUserReturns struct { + A *model.User + B *model.AppError +} + +func (g *APIRPCClient) UpdateUser(user *model.User) (*model.User, *model.AppError) { + _args := &UpdateUserArgs{user} + _returns := &UpdateUserReturns{} + if err := g.client.Call("Plugin.UpdateUser", _args, _returns); err != nil { + g.log.Error("RPC call to UpdateUser API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) UpdateUser(args *UpdateUserArgs, returns *UpdateUserReturns) error { + if hook, ok := s.impl.(interface { + UpdateUser(user *model.User) (*model.User, *model.AppError) + }); ok { + returns.A, returns.B = hook.UpdateUser(args.A) + } + return nil +} + +type CreateTeamArgs struct { + A *model.Team +} + +type CreateTeamReturns struct { + A *model.Team + B *model.AppError +} + +func (g *APIRPCClient) CreateTeam(team *model.Team) (*model.Team, *model.AppError) { + _args := &CreateTeamArgs{team} + _returns := &CreateTeamReturns{} + if err := g.client.Call("Plugin.CreateTeam", _args, _returns); err != nil { + g.log.Error("RPC call to CreateTeam API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) CreateTeam(args *CreateTeamArgs, returns *CreateTeamReturns) error { + if hook, ok := s.impl.(interface { + CreateTeam(team *model.Team) (*model.Team, *model.AppError) + }); ok { + returns.A, returns.B = hook.CreateTeam(args.A) + } + return nil +} + +type DeleteTeamArgs struct { + A string +} + +type DeleteTeamReturns struct { + A *model.AppError +} + +func (g *APIRPCClient) DeleteTeam(teamId string) *model.AppError { + _args := &DeleteTeamArgs{teamId} + _returns := &DeleteTeamReturns{} + if err := g.client.Call("Plugin.DeleteTeam", _args, _returns); err != nil { + g.log.Error("RPC call to DeleteTeam API failed.", mlog.Err(err)) + } + return _returns.A +} + +func (s *APIRPCServer) DeleteTeam(args *DeleteTeamArgs, returns *DeleteTeamReturns) error { + if hook, ok := s.impl.(interface { + DeleteTeam(teamId string) *model.AppError + }); ok { + returns.A = hook.DeleteTeam(args.A) + } + return nil +} + +type GetTeamArgs struct { + A string +} + +type GetTeamReturns struct { + A *model.Team + B *model.AppError +} + +func (g *APIRPCClient) GetTeam(teamId string) (*model.Team, *model.AppError) { + _args := &GetTeamArgs{teamId} + _returns := &GetTeamReturns{} + if err := g.client.Call("Plugin.GetTeam", _args, _returns); err != nil { + g.log.Error("RPC call to GetTeam API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetTeam(args *GetTeamArgs, returns *GetTeamReturns) error { + if hook, ok := s.impl.(interface { + GetTeam(teamId string) (*model.Team, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetTeam(args.A) + } + return nil +} + +type GetTeamByNameArgs struct { + A string +} + +type GetTeamByNameReturns struct { + A *model.Team + B *model.AppError +} + +func (g *APIRPCClient) GetTeamByName(name string) (*model.Team, *model.AppError) { + _args := &GetTeamByNameArgs{name} + _returns := &GetTeamByNameReturns{} + if err := g.client.Call("Plugin.GetTeamByName", _args, _returns); err != nil { + g.log.Error("RPC call to GetTeamByName API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetTeamByName(args *GetTeamByNameArgs, returns *GetTeamByNameReturns) error { + if hook, ok := s.impl.(interface { + GetTeamByName(name string) (*model.Team, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetTeamByName(args.A) + } + return nil +} + +type UpdateTeamArgs struct { + A *model.Team +} + +type UpdateTeamReturns struct { + A *model.Team + B *model.AppError +} + +func (g *APIRPCClient) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) { + _args := &UpdateTeamArgs{team} + _returns := &UpdateTeamReturns{} + if err := g.client.Call("Plugin.UpdateTeam", _args, _returns); err != nil { + g.log.Error("RPC call to UpdateTeam API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) UpdateTeam(args *UpdateTeamArgs, returns *UpdateTeamReturns) error { + if hook, ok := s.impl.(interface { + UpdateTeam(team *model.Team) (*model.Team, *model.AppError) + }); ok { + returns.A, returns.B = hook.UpdateTeam(args.A) + } + return nil +} + +type CreateChannelArgs struct { + A *model.Channel +} + +type CreateChannelReturns struct { + A *model.Channel + B *model.AppError +} + +func (g *APIRPCClient) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { + _args := &CreateChannelArgs{channel} + _returns := &CreateChannelReturns{} + if err := g.client.Call("Plugin.CreateChannel", _args, _returns); err != nil { + g.log.Error("RPC call to CreateChannel API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) CreateChannel(args *CreateChannelArgs, returns *CreateChannelReturns) error { + if hook, ok := s.impl.(interface { + CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) + }); ok { + returns.A, returns.B = hook.CreateChannel(args.A) + } + return nil +} + +type DeleteChannelArgs struct { + A string +} + +type DeleteChannelReturns struct { + A *model.AppError +} + +func (g *APIRPCClient) DeleteChannel(channelId string) *model.AppError { + _args := &DeleteChannelArgs{channelId} + _returns := &DeleteChannelReturns{} + if err := g.client.Call("Plugin.DeleteChannel", _args, _returns); err != nil { + g.log.Error("RPC call to DeleteChannel API failed.", mlog.Err(err)) + } + return _returns.A +} + +func (s *APIRPCServer) DeleteChannel(args *DeleteChannelArgs, returns *DeleteChannelReturns) error { + if hook, ok := s.impl.(interface { + DeleteChannel(channelId string) *model.AppError + }); ok { + returns.A = hook.DeleteChannel(args.A) + } + return nil +} + +type GetChannelArgs struct { + A string +} + +type GetChannelReturns struct { + A *model.Channel + B *model.AppError +} + +func (g *APIRPCClient) GetChannel(channelId string) (*model.Channel, *model.AppError) { + _args := &GetChannelArgs{channelId} + _returns := &GetChannelReturns{} + if err := g.client.Call("Plugin.GetChannel", _args, _returns); err != nil { + g.log.Error("RPC call to GetChannel API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetChannel(args *GetChannelArgs, returns *GetChannelReturns) error { + if hook, ok := s.impl.(interface { + GetChannel(channelId string) (*model.Channel, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetChannel(args.A) + } + return nil +} + +type GetChannelByNameArgs struct { + A string + B string +} + +type GetChannelByNameReturns struct { + A *model.Channel + B *model.AppError +} + +func (g *APIRPCClient) GetChannelByName(name, teamId string) (*model.Channel, *model.AppError) { + _args := &GetChannelByNameArgs{name, teamId} + _returns := &GetChannelByNameReturns{} + if err := g.client.Call("Plugin.GetChannelByName", _args, _returns); err != nil { + g.log.Error("RPC call to GetChannelByName API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetChannelByName(args *GetChannelByNameArgs, returns *GetChannelByNameReturns) error { + if hook, ok := s.impl.(interface { + GetChannelByName(name, teamId string) (*model.Channel, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetChannelByName(args.A, args.B) + } + return nil +} + +type GetDirectChannelArgs struct { + A string + B string +} + +type GetDirectChannelReturns struct { + A *model.Channel + B *model.AppError +} + +func (g *APIRPCClient) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) { + _args := &GetDirectChannelArgs{userId1, userId2} + _returns := &GetDirectChannelReturns{} + if err := g.client.Call("Plugin.GetDirectChannel", _args, _returns); err != nil { + g.log.Error("RPC call to GetDirectChannel API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetDirectChannel(args *GetDirectChannelArgs, returns *GetDirectChannelReturns) error { + if hook, ok := s.impl.(interface { + GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetDirectChannel(args.A, args.B) + } + return nil +} + +type GetGroupChannelArgs struct { + A []string +} + +type GetGroupChannelReturns struct { + A *model.Channel + B *model.AppError +} + +func (g *APIRPCClient) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) { + _args := &GetGroupChannelArgs{userIds} + _returns := &GetGroupChannelReturns{} + if err := g.client.Call("Plugin.GetGroupChannel", _args, _returns); err != nil { + g.log.Error("RPC call to GetGroupChannel API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetGroupChannel(args *GetGroupChannelArgs, returns *GetGroupChannelReturns) error { + if hook, ok := s.impl.(interface { + GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetGroupChannel(args.A) + } + return nil +} + +type UpdateChannelArgs struct { + A *model.Channel +} + +type UpdateChannelReturns struct { + A *model.Channel + B *model.AppError +} + +func (g *APIRPCClient) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { + _args := &UpdateChannelArgs{channel} + _returns := &UpdateChannelReturns{} + if err := g.client.Call("Plugin.UpdateChannel", _args, _returns); err != nil { + g.log.Error("RPC call to UpdateChannel API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) UpdateChannel(args *UpdateChannelArgs, returns *UpdateChannelReturns) error { + if hook, ok := s.impl.(interface { + UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) + }); ok { + returns.A, returns.B = hook.UpdateChannel(args.A) + } + return nil +} + +type AddChannelMemberArgs struct { + A string + B string +} + +type AddChannelMemberReturns struct { + A *model.ChannelMember + B *model.AppError +} + +func (g *APIRPCClient) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) { + _args := &AddChannelMemberArgs{channelId, userId} + _returns := &AddChannelMemberReturns{} + if err := g.client.Call("Plugin.AddChannelMember", _args, _returns); err != nil { + g.log.Error("RPC call to AddChannelMember API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) AddChannelMember(args *AddChannelMemberArgs, returns *AddChannelMemberReturns) error { + if hook, ok := s.impl.(interface { + AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) + }); ok { + returns.A, returns.B = hook.AddChannelMember(args.A, args.B) + } + return nil +} + +type GetChannelMemberArgs struct { + A string + B string +} + +type GetChannelMemberReturns struct { + A *model.ChannelMember + B *model.AppError +} + +func (g *APIRPCClient) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) { + _args := &GetChannelMemberArgs{channelId, userId} + _returns := &GetChannelMemberReturns{} + if err := g.client.Call("Plugin.GetChannelMember", _args, _returns); err != nil { + g.log.Error("RPC call to GetChannelMember API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetChannelMember(args *GetChannelMemberArgs, returns *GetChannelMemberReturns) error { + if hook, ok := s.impl.(interface { + GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetChannelMember(args.A, args.B) + } + return nil +} + +type UpdateChannelMemberRolesArgs struct { + A string + B string + C string +} + +type UpdateChannelMemberRolesReturns struct { + A *model.ChannelMember + B *model.AppError +} + +func (g *APIRPCClient) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) { + _args := &UpdateChannelMemberRolesArgs{channelId, userId, newRoles} + _returns := &UpdateChannelMemberRolesReturns{} + if err := g.client.Call("Plugin.UpdateChannelMemberRoles", _args, _returns); err != nil { + g.log.Error("RPC call to UpdateChannelMemberRoles API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) UpdateChannelMemberRoles(args *UpdateChannelMemberRolesArgs, returns *UpdateChannelMemberRolesReturns) error { + if hook, ok := s.impl.(interface { + UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) + }); ok { + returns.A, returns.B = hook.UpdateChannelMemberRoles(args.A, args.B, args.C) + } + return nil +} + +type UpdateChannelMemberNotificationsArgs struct { + A string + B string + C map[string]string +} + +type UpdateChannelMemberNotificationsReturns struct { + A *model.ChannelMember + B *model.AppError +} + +func (g *APIRPCClient) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) { + _args := &UpdateChannelMemberNotificationsArgs{channelId, userId, notifications} + _returns := &UpdateChannelMemberNotificationsReturns{} + if err := g.client.Call("Plugin.UpdateChannelMemberNotifications", _args, _returns); err != nil { + g.log.Error("RPC call to UpdateChannelMemberNotifications API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) UpdateChannelMemberNotifications(args *UpdateChannelMemberNotificationsArgs, returns *UpdateChannelMemberNotificationsReturns) error { + if hook, ok := s.impl.(interface { + UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) + }); ok { + returns.A, returns.B = hook.UpdateChannelMemberNotifications(args.A, args.B, args.C) + } + return nil +} + +type DeleteChannelMemberArgs struct { + A string + B string +} + +type DeleteChannelMemberReturns struct { + A *model.AppError +} + +func (g *APIRPCClient) DeleteChannelMember(channelId, userId string) *model.AppError { + _args := &DeleteChannelMemberArgs{channelId, userId} + _returns := &DeleteChannelMemberReturns{} + if err := g.client.Call("Plugin.DeleteChannelMember", _args, _returns); err != nil { + g.log.Error("RPC call to DeleteChannelMember API failed.", mlog.Err(err)) + } + return _returns.A +} + +func (s *APIRPCServer) DeleteChannelMember(args *DeleteChannelMemberArgs, returns *DeleteChannelMemberReturns) error { + if hook, ok := s.impl.(interface { + DeleteChannelMember(channelId, userId string) *model.AppError + }); ok { + returns.A = hook.DeleteChannelMember(args.A, args.B) + } + return nil +} + +type CreatePostArgs struct { + A *model.Post +} + +type CreatePostReturns struct { + A *model.Post + B *model.AppError +} + +func (g *APIRPCClient) CreatePost(post *model.Post) (*model.Post, *model.AppError) { + _args := &CreatePostArgs{post} + _returns := &CreatePostReturns{} + if err := g.client.Call("Plugin.CreatePost", _args, _returns); err != nil { + g.log.Error("RPC call to CreatePost API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) CreatePost(args *CreatePostArgs, returns *CreatePostReturns) error { + if hook, ok := s.impl.(interface { + CreatePost(post *model.Post) (*model.Post, *model.AppError) + }); ok { + returns.A, returns.B = hook.CreatePost(args.A) + } + return nil +} + +type DeletePostArgs struct { + A string +} + +type DeletePostReturns struct { + A *model.AppError +} + +func (g *APIRPCClient) DeletePost(postId string) *model.AppError { + _args := &DeletePostArgs{postId} + _returns := &DeletePostReturns{} + if err := g.client.Call("Plugin.DeletePost", _args, _returns); err != nil { + g.log.Error("RPC call to DeletePost API failed.", mlog.Err(err)) + } + return _returns.A +} + +func (s *APIRPCServer) DeletePost(args *DeletePostArgs, returns *DeletePostReturns) error { + if hook, ok := s.impl.(interface { + DeletePost(postId string) *model.AppError + }); ok { + returns.A = hook.DeletePost(args.A) + } + return nil +} + +type GetPostArgs struct { + A string +} + +type GetPostReturns struct { + A *model.Post + B *model.AppError +} + +func (g *APIRPCClient) GetPost(postId string) (*model.Post, *model.AppError) { + _args := &GetPostArgs{postId} + _returns := &GetPostReturns{} + if err := g.client.Call("Plugin.GetPost", _args, _returns); err != nil { + g.log.Error("RPC call to GetPost API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) GetPost(args *GetPostArgs, returns *GetPostReturns) error { + if hook, ok := s.impl.(interface { + GetPost(postId string) (*model.Post, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetPost(args.A) + } + return nil +} + +type UpdatePostArgs struct { + A *model.Post +} + +type UpdatePostReturns struct { + A *model.Post + B *model.AppError +} + +func (g *APIRPCClient) UpdatePost(post *model.Post) (*model.Post, *model.AppError) { + _args := &UpdatePostArgs{post} + _returns := &UpdatePostReturns{} + if err := g.client.Call("Plugin.UpdatePost", _args, _returns); err != nil { + g.log.Error("RPC call to UpdatePost API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) UpdatePost(args *UpdatePostArgs, returns *UpdatePostReturns) error { + if hook, ok := s.impl.(interface { + UpdatePost(post *model.Post) (*model.Post, *model.AppError) + }); ok { + returns.A, returns.B = hook.UpdatePost(args.A) + } + return nil +} + +type KVSetArgs struct { + A string + B []byte +} + +type KVSetReturns struct { + A *model.AppError +} + +func (g *APIRPCClient) KVSet(key string, value []byte) *model.AppError { + _args := &KVSetArgs{key, value} + _returns := &KVSetReturns{} + if err := g.client.Call("Plugin.KVSet", _args, _returns); err != nil { + g.log.Error("RPC call to KVSet API failed.", mlog.Err(err)) + } + return _returns.A +} + +func (s *APIRPCServer) KVSet(args *KVSetArgs, returns *KVSetReturns) error { + if hook, ok := s.impl.(interface { + KVSet(key string, value []byte) *model.AppError + }); ok { + returns.A = hook.KVSet(args.A, args.B) + } + return nil +} + +type KVGetArgs struct { + A string +} + +type KVGetReturns struct { + A []byte + B *model.AppError +} + +func (g *APIRPCClient) KVGet(key string) ([]byte, *model.AppError) { + _args := &KVGetArgs{key} + _returns := &KVGetReturns{} + if err := g.client.Call("Plugin.KVGet", _args, _returns); err != nil { + g.log.Error("RPC call to KVGet API failed.", mlog.Err(err)) + } + return _returns.A, _returns.B +} + +func (s *APIRPCServer) KVGet(args *KVGetArgs, returns *KVGetReturns) error { + if hook, ok := s.impl.(interface { + KVGet(key string) ([]byte, *model.AppError) + }); ok { + returns.A, returns.B = hook.KVGet(args.A) + } + return nil +} + +type KVDeleteArgs struct { + A string +} + +type KVDeleteReturns struct { + A *model.AppError +} + +func (g *APIRPCClient) KVDelete(key string) *model.AppError { + _args := &KVDeleteArgs{key} + _returns := &KVDeleteReturns{} + if err := g.client.Call("Plugin.KVDelete", _args, _returns); err != nil { + g.log.Error("RPC call to KVDelete API failed.", mlog.Err(err)) + } + return _returns.A +} + +func (s *APIRPCServer) KVDelete(args *KVDeleteArgs, returns *KVDeleteReturns) error { + if hook, ok := s.impl.(interface { + KVDelete(key string) *model.AppError + }); ok { + returns.A = hook.KVDelete(args.A) + } + return nil +} diff --git a/plugin/environment.go b/plugin/environment.go new file mode 100644 index 000000000..de67225d2 --- /dev/null +++ b/plugin/environment.go @@ -0,0 +1,260 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "sync" + + "github.com/mattermost/mattermost-server/mlog" + "github.com/mattermost/mattermost-server/model" + "github.com/pkg/errors" +) + +type APIImplCreatorFunc func(*model.Manifest) API +type SupervisorCreatorFunc func(*model.BundleInfo, *mlog.Logger, API) (*Supervisor, error) + +// Hooks will be the hooks API for the plugin +// Return value should be true if we should continue calling more plugins +type MultliPluginHookRunnerFunc func(hooks Hooks) bool + +type ActivePlugin struct { + BundleInfo *model.BundleInfo + State int + Supervisor *Supervisor +} + +type Environment struct { + activePlugins map[string]ActivePlugin + mutex sync.RWMutex + logger *mlog.Logger + newAPIImpl APIImplCreatorFunc + pluginDir string + webappPluginDir string +} + +func NewEnvironment(newAPIImpl APIImplCreatorFunc, pluginDir string, webappPluginDir string, logger *mlog.Logger) (*Environment, error) { + return &Environment{ + activePlugins: make(map[string]ActivePlugin), + logger: logger, + newAPIImpl: newAPIImpl, + pluginDir: pluginDir, + webappPluginDir: webappPluginDir, + }, nil +} + +// Performs a full scan of the given path. +// +// This function will return info for all subdirectories that appear to be plugins (i.e. all +// subdirectories containing plugin manifest files, regardless of whether they could actually be +// parsed). +// +// Plugins are found non-recursively and paths beginning with a dot are always ignored. +func ScanSearchPath(path string) ([]*model.BundleInfo, error) { + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + var ret []*model.BundleInfo + for _, file := range files { + if !file.IsDir() || file.Name()[0] == '.' { + continue + } + if info := model.BundleInfoForPath(filepath.Join(path, file.Name())); info.ManifestPath != "" { + ret = append(ret, info) + } + } + return ret, nil +} + +// Returns a list of all plugins within the environment. +func (env *Environment) Available() ([]*model.BundleInfo, error) { + return ScanSearchPath(env.pluginDir) +} + +// Returns a list of all currently active plugins within the environment. +func (env *Environment) Active() []*model.BundleInfo { + env.mutex.RLock() + defer env.mutex.RUnlock() + + activePlugins := []*model.BundleInfo{} + for _, p := range env.activePlugins { + activePlugins = append(activePlugins, p.BundleInfo) + } + + return activePlugins +} + +func (env *Environment) IsActive(id string) bool { + _, ok := env.activePlugins[id] + return ok +} + +// Returns a list of plugin statuses reprensenting the state of every plugin +func (env *Environment) Statuses() (model.PluginStatuses, error) { + env.mutex.RLock() + defer env.mutex.RUnlock() + + plugins, err := env.Available() + if err != nil { + return nil, errors.Wrap(err, "unable to get plugin statuses") + } + + pluginStatuses := make(model.PluginStatuses, 0, len(plugins)) + for _, plugin := range plugins { + // For now we don't handle bad manifests, we should + if plugin.Manifest == nil { + continue + } + + pluginState := model.PluginStateNotRunning + if plugin, ok := env.activePlugins[plugin.Manifest.Id]; ok { + pluginState = plugin.State + } + + status := &model.PluginStatus{ + PluginId: plugin.Manifest.Id, + PluginPath: filepath.Dir(plugin.ManifestPath), + State: pluginState, + Name: plugin.Manifest.Name, + Description: plugin.Manifest.Description, + Version: plugin.Manifest.Version, + } + + pluginStatuses = append(pluginStatuses, status) + } + + return pluginStatuses, nil +} + +func (env *Environment) Activate(id string) (reterr error) { + env.mutex.Lock() + defer env.mutex.Unlock() + + // Check if we are already active + if _, ok := env.activePlugins[id]; ok { + return nil + } + + plugins, err := env.Available() + if err != nil { + return err + } + var pluginInfo *model.BundleInfo + for _, p := range plugins { + if p.Manifest != nil && p.Manifest.Id == id { + if pluginInfo != nil { + return fmt.Errorf("multiple plugins found: %v", id) + } + pluginInfo = p + } + } + if pluginInfo == nil { + return fmt.Errorf("plugin not found: %v", id) + } + + activePlugin := ActivePlugin{BundleInfo: pluginInfo} + defer func() { + if reterr == nil { + activePlugin.State = model.PluginStateRunning + } else { + activePlugin.State = model.PluginStateFailedToStart + } + env.activePlugins[pluginInfo.Manifest.Id] = activePlugin + }() + + if pluginInfo.Manifest.Webapp != nil { + bundlePath := filepath.Clean(pluginInfo.Manifest.Webapp.BundlePath) + if bundlePath == "" || bundlePath[0] == '.' { + return fmt.Errorf("invalid webapp bundle path") + } + bundlePath = filepath.Join(env.pluginDir, id, bundlePath) + + webappBundle, err := ioutil.ReadFile(bundlePath) + if err != nil { + return errors.Wrapf(err, "unable to read webapp bundle: %v", id) + } + + err = ioutil.WriteFile(fmt.Sprintf("%s/%s_bundle.js", env.webappPluginDir, id), webappBundle, 0644) + if err != nil { + return errors.Wrapf(err, "unable to write webapp bundle: %v", id) + } + } + + if pluginInfo.Manifest.Backend != nil { + supervisor, err := NewSupervisor(pluginInfo, env.logger, env.newAPIImpl(pluginInfo.Manifest)) + if err != nil { + return errors.Wrapf(err, "unable to start plugin: %v", id) + } + activePlugin.Supervisor = supervisor + } + + return nil +} + +// Deactivates the plugin with the given id. +func (env *Environment) Deactivate(id string) { + env.mutex.Lock() + defer env.mutex.Unlock() + + if activePlugin, ok := env.activePlugins[id]; !ok { + return + } else { + delete(env.activePlugins, id) + if activePlugin.Supervisor != nil { + if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil { + env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", activePlugin.BundleInfo.Manifest.Id), mlog.Err(err)) + } + activePlugin.Supervisor.Shutdown() + } + } +} + +// Deactivates all plugins and gracefully shuts down the environment. +func (env *Environment) Shutdown() { + env.mutex.Lock() + defer env.mutex.Unlock() + + for _, activePlugin := range env.activePlugins { + if activePlugin.Supervisor != nil { + if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil { + env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", activePlugin.BundleInfo.Manifest.Id), mlog.Err(err)) + } + activePlugin.Supervisor.Shutdown() + } + } + env.activePlugins = make(map[string]ActivePlugin) + return +} + +// Returns the hooks API for the plugin ID specified +// You should probably use RunMultiPluginHook instead. +func (env *Environment) HooksForPlugin(id string) (Hooks, error) { + env.mutex.RLock() + defer env.mutex.RUnlock() + + if plug, ok := env.activePlugins[id]; ok && plug.Supervisor != nil { + return plug.Supervisor.Hooks(), nil + } + + return nil, fmt.Errorf("plugin not found: %v", id) +} + +// Calls hookRunnerFunc with the hooks for each active plugin that implments the given HookId +// If hookRunnerFunc returns false, then iteration will not continue. +func (env *Environment) RunMultiPluginHook(hookRunnerFunc MultliPluginHookRunnerFunc, mustImplement int) { + env.mutex.RLock() + defer env.mutex.RUnlock() + + for _, activePlugin := range env.activePlugins { + if activePlugin.Supervisor == nil || !activePlugin.Supervisor.Implements(mustImplement) { + continue + } + if !hookRunnerFunc(activePlugin.Supervisor.Hooks()) { + break + } + } +} diff --git a/plugin/example_hello_user_test.go b/plugin/example_hello_user_test.go deleted file mode 100644 index 989cca0f2..000000000 --- a/plugin/example_hello_user_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package plugin_test - -import ( - "fmt" - "net/http" - - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/plugin/rpcplugin" -) - -type HelloUserPlugin struct { - api plugin.API -} - -func (p *HelloUserPlugin) OnActivate(api plugin.API) error { - // Just save api for later when we need to look up users. - p.api = api - return nil -} - -func (p *HelloUserPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if userId := r.Header.Get("Mattermost-User-Id"); userId == "" { - // Our visitor is unauthenticated. - fmt.Fprintf(w, "Hello, stranger!") - } else if user, err := p.api.GetUser(userId); err == nil { - // Greet the user by name! - fmt.Fprintf(w, "Welcome back, %v!", user.Username) - } else { - // This won't happen in normal circumstances, but let's just be safe. - w.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(w, err.Error()) - } -} - -// This example demonstrates a plugin that handles HTTP requests which respond by greeting the user -// by name. -func Example_helloUser() { - rpcplugin.Main(&HelloUserPlugin{}) -} diff --git a/plugin/example_hello_world_test.go b/plugin/example_hello_world_test.go deleted file mode 100644 index 5dea28823..000000000 --- a/plugin/example_hello_world_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package plugin_test - -import ( - "fmt" - "net/http" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin" -) - -type HelloWorldPlugin struct{} - -func (p *HelloWorldPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, world!") -} - -// This example demonstrates a plugin that handles HTTP requests which respond by greeting the -// world. -func Example_helloWorld() { - rpcplugin.Main(&HelloWorldPlugin{}) -} diff --git a/plugin/example_test.go b/plugin/example_test.go deleted file mode 100644 index e6ae3c2ea..000000000 --- a/plugin/example_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package plugin_test - -import ( - "io/ioutil" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin/plugintest" -) - -func TestHelloUserPlugin(t *testing.T) { - user := &model.User{ - Id: model.NewId(), - Username: "billybob", - } - - api := &plugintest.API{} - api.On("GetUser", user.Id).Return(user, nil) - defer api.AssertExpectations(t) - - p := &HelloUserPlugin{} - p.OnActivate(api) - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "/", nil) - r.Header.Add("Mattermost-User-Id", user.Id) - p.ServeHTTP(w, r) - body, err := ioutil.ReadAll(w.Result().Body) - require.NoError(t, err) - assert.Equal(t, "Welcome back, billybob!", string(body)) -} diff --git a/plugin/hclog_adapter.go b/plugin/hclog_adapter.go new file mode 100644 index 000000000..c8e39877e --- /dev/null +++ b/plugin/hclog_adapter.go @@ -0,0 +1,73 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin + +import ( + "fmt" + "log" + + "github.com/hashicorp/go-hclog" + "github.com/mattermost/mattermost-server/mlog" +) + +type HclogAdapter struct { + wrappedLogger *mlog.Logger + extrasKey string +} + +func (h *HclogAdapter) Trace(msg string, args ...interface{}) { + h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...))) +} + +func (h *HclogAdapter) Debug(msg string, args ...interface{}) { + h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...))) +} + +func (h *HclogAdapter) Info(msg string, args ...interface{}) { + h.wrappedLogger.Info(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...))) +} + +func (h *HclogAdapter) Warn(msg string, args ...interface{}) { + h.wrappedLogger.Warn(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...))) +} + +func (h *HclogAdapter) Error(msg string, args ...interface{}) { + h.wrappedLogger.Error(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...))) +} + +func (h *HclogAdapter) IsTrace() bool { + return false +} + +func (h *HclogAdapter) IsDebug() bool { + return true +} + +func (h *HclogAdapter) IsInfo() bool { + return true +} + +func (h *HclogAdapter) IsWarn() bool { + return true +} + +func (h *HclogAdapter) IsError() bool { + return true +} + +func (h *HclogAdapter) With(args ...interface{}) hclog.Logger { + return h +} + +func (h *HclogAdapter) Named(name string) hclog.Logger { + return h +} + +func (h *HclogAdapter) ResetNamed(name string) hclog.Logger { + return h +} + +func (h *HclogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger { + return h.wrappedLogger.StdLog() +} diff --git a/plugin/hooks.go b/plugin/hooks.go index e41081e48..68eca8ede 100644 --- a/plugin/hooks.go +++ b/plugin/hooks.go @@ -9,15 +9,32 @@ import ( "github.com/mattermost/mattermost-server/model" ) +// These assignments are part of the wire protocol. You can add more, but should not change existing +// assignments. Follow the naming convention of Id as the autogenerated glue code depends on that. +const ( + OnActivateId = 0 + OnDeactivateId = 1 + ServeHTTPId = 2 + OnConfigurationChangeId = 3 + ExecuteCommandId = 4 + MessageWillBePostedId = 5 + MessageWillBeUpdatedId = 6 + MessageHasBeenPostedId = 7 + MessageHasBeenUpdatedId = 8 + TotalHooksId = iota +) + // Methods from the Hooks interface can be used by a plugin to respond to events. Methods are likely // to be added over time, and plugins are not expected to implement all of them. Instead, plugins // are expected to implement a subset of them and pass an instance to plugin/rpcplugin.Main, which // will take over execution of the process and add default behaviors for missing hooks. type Hooks interface { - // OnActivate is invoked when the plugin is activated. Implementations will usually want to save - // the api argument for later use. Loading configuration for the first time is also a commonly - // done here. - OnActivate(API) error + // OnActivate is invoked when the plugin is activated. + OnActivate() error + + // Implemented returns a list of hooks that are implmented by the plugin. + // Plugins do not need to provide an implementation. Any given will be ignored. + Implemented() ([]string, error) // OnDeactivate is invoked when the plugin is deactivated. This is the plugin's last chance to // use the API, and the plugin will be terminated shortly after this invocation. @@ -31,7 +48,7 @@ type Hooks interface { // // The Mattermost-User-Id header will be present if (and only if) the request is by an // authenticated user. - ServeHTTP(http.ResponseWriter, *http.Request) + ServeHTTP(w http.ResponseWriter, r *http.Request) // ExecuteCommand executes a command that has been previously registered via the RegisterCommand // API. diff --git a/plugin/http.go b/plugin/http.go new file mode 100644 index 000000000..5faf8f08a --- /dev/null +++ b/plugin/http.go @@ -0,0 +1,91 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin + +import ( + "io" + "net/http" + "net/rpc" +) + +type HTTPResponseWriterRPCServer struct { + w http.ResponseWriter +} + +func (w *HTTPResponseWriterRPCServer) Header(args struct{}, reply *http.Header) error { + *reply = w.w.Header() + return nil +} + +func (w *HTTPResponseWriterRPCServer) Write(args []byte, reply *struct{}) error { + _, err := w.w.Write(args) + return err +} + +func (w *HTTPResponseWriterRPCServer) WriteHeader(args int, reply *struct{}) error { + w.w.WriteHeader(args) + return nil +} + +func (w *HTTPResponseWriterRPCServer) SyncHeader(args http.Header, reply *struct{}) error { + dest := w.w.Header() + for k := range dest { + if _, ok := args[k]; !ok { + delete(dest, k) + } + } + for k, v := range args { + dest[k] = v + } + return nil +} + +func ServeHTTPResponseWriter(w http.ResponseWriter, conn io.ReadWriteCloser) { + server := rpc.NewServer() + server.Register(&HTTPResponseWriterRPCServer{ + w: w, + }) + server.ServeConn(conn) +} + +type HTTPResponseWriterRPCClient struct { + client *rpc.Client + header http.Header +} + +var _ http.ResponseWriter = (*HTTPResponseWriterRPCClient)(nil) + +func (w *HTTPResponseWriterRPCClient) Header() http.Header { + if w.header == nil { + w.client.Call("Plugin.Header", struct{}{}, &w.header) + } + return w.header +} + +func (w *HTTPResponseWriterRPCClient) Write(b []byte) (int, error) { + if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil { + return 0, err + } + if err := w.client.Call("Plugin.Write", b, nil); err != nil { + return 0, err + } + return len(b), nil +} + +func (w *HTTPResponseWriterRPCClient) WriteHeader(statusCode int) { + if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil { + return + } + w.client.Call("Plugin.WriteHeader", statusCode, nil) +} + +func (h *HTTPResponseWriterRPCClient) Close() error { + return h.client.Close() +} + +func ConnectHTTPResponseWriter(conn io.ReadWriteCloser) *HTTPResponseWriterRPCClient { + return &HTTPResponseWriterRPCClient{ + client: rpc.NewClient(conn), + } +} diff --git a/plugin/interface_generator/main.go b/plugin/interface_generator/main.go new file mode 100644 index 000000000..5f66506d3 --- /dev/null +++ b/plugin/interface_generator/main.go @@ -0,0 +1,377 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/alecthomas/template" + "github.com/pkg/errors" +) + +type IHookEntry struct { + FuncName string + Args *ast.FieldList + Results *ast.FieldList +} + +type PluginInterfaceInfo struct { + Hooks []IHookEntry + API []IHookEntry + FileSet *token.FileSet +} + +func FieldListToFuncList(fieldList *ast.FieldList, fileset *token.FileSet) string { + result := []string{} + if fieldList == nil || len(fieldList.List) == 0 { + return "()" + } + for _, field := range fieldList.List { + typeNameBuffer := &bytes.Buffer{} + err := printer.Fprint(typeNameBuffer, fileset, field.Type) + if err != nil { + panic(err) + } + typeName := typeNameBuffer.String() + names := []string{} + for _, name := range field.Names { + names = append(names, name.Name) + } + result = append(result, strings.Join(names, ", ")+" "+typeName) + } + + return "(" + strings.Join(result, ", ") + ")" +} + +func FieldListToNames(fieldList *ast.FieldList, fileset *token.FileSet) string { + result := []string{} + if fieldList == nil || len(fieldList.List) == 0 { + return "" + } + for _, field := range fieldList.List { + for _, name := range field.Names { + result = append(result, name.Name) + } + } + + return strings.Join(result, ", ") +} + +func FieldListDestruct(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string { + result := []string{} + if fieldList == nil || len(fieldList.List) == 0 { + return "" + } + nextLetter := 'A' + for _, field := range fieldList.List { + if len(field.Names) == 0 { + result = append(result, structPrefix+string(nextLetter)) + nextLetter += 1 + } else { + for range field.Names { + result = append(result, structPrefix+string(nextLetter)) + nextLetter += 1 + } + } + } + + return strings.Join(result, ", ") +} + +func FieldListToStructList(fieldList *ast.FieldList, fileset *token.FileSet) string { + result := []string{} + if fieldList == nil || len(fieldList.List) == 0 { + return "" + } + nextLetter := 'A' + for _, field := range fieldList.List { + typeNameBuffer := &bytes.Buffer{} + err := printer.Fprint(typeNameBuffer, fileset, field.Type) + if err != nil { + panic(err) + } + typeName := typeNameBuffer.String() + if len(field.Names) == 0 { + result = append(result, string(nextLetter)+" "+typeName) + nextLetter += 1 + } else { + for range field.Names { + result = append(result, string(nextLetter)+" "+typeName) + nextLetter += 1 + } + } + } + + return strings.Join(result, "\n\t") +} + +func goList(dir string) ([]string, error) { + cmd := exec.Command("go", "list", "-f", "{{.Dir}}", dir) + bytes, err := cmd.Output() + if err != nil { + return nil, errors.Wrap(err, "Can't list packages") + } + + return strings.Fields(string(bytes)), nil +} + +func (info *PluginInterfaceInfo) addHookMethod(method *ast.Field) { + info.Hooks = append(info.Hooks, IHookEntry{ + FuncName: method.Names[0].Name, + Args: method.Type.(*ast.FuncType).Params, + Results: method.Type.(*ast.FuncType).Results, + }) +} + +func (info *PluginInterfaceInfo) addAPIMethod(method *ast.Field) { + info.API = append(info.API, IHookEntry{ + FuncName: method.Names[0].Name, + Args: method.Type.(*ast.FuncType).Params, + Results: method.Type.(*ast.FuncType).Results, + }) +} + +func (info *PluginInterfaceInfo) makeHookInspector() func(node ast.Node) bool { + return func(node ast.Node) bool { + if typeSpec, ok := node.(*ast.TypeSpec); ok { + if typeSpec.Name.Name == "Hooks" { + for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List { + info.addHookMethod(method) + } + return false + } else if typeSpec.Name.Name == "API" { + for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List { + info.addAPIMethod(method) + } + return false + } + } + return true + } +} + +func getPluginInfo(dir string) (*PluginInterfaceInfo, error) { + pluginInfo := &PluginInterfaceInfo{ + Hooks: make([]IHookEntry, 0), + FileSet: token.NewFileSet(), + } + + packages, err := parser.ParseDir(pluginInfo.FileSet, dir, nil, parser.ParseComments) + if err != nil { + log.Println("Parser error in dir "+dir+": ", err) + } + + for _, pkg := range packages { + if pkg.Name != "plugin" { + continue + } + + for _, file := range pkg.Files { + ast.Inspect(file, pluginInfo.makeHookInspector()) + } + } + + return pluginInfo, nil +} + +var hooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// Code generated by "make pluginapi" +// DO NOT EDIT + +package plugin + +{{range .HooksMethods}} + +func init() { + HookNameToId["{{.Name}}"] = {{.Name}}Id +} + +type {{.Name}}Args struct { + {{structStyle .Params}} +} + +type {{.Name}}Returns struct { + {{structStyle .Return}} +} + +func (g *HooksRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { + _args := &{{.Name}}Args{ {{valuesOnly .Params}} } + _returns := &{{.Name}}Returns{} + if g.implemented[{{.Name}}Id] { + if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil { + g.log.Error("RPC call {{.Name}} to plugin failed.", mlog.Err(err)) + } + } + return {{destruct "_returns." .Return}} +} + +func (s *HooksRPCServer) {{.Name}}(args *{{.Name}}Args, returns *{{.Name}}Returns) error { + if hook, ok := s.impl.(interface { + {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} + }); ok { + {{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}}) + } + return nil +} +{{end}} + +{{range .APIMethods}} + +type {{.Name}}Args struct { + {{structStyle .Params}} +} + +type {{.Name}}Returns struct { + {{structStyle .Return}} +} + +func (g *APIRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} { + _args := &{{.Name}}Args{ {{valuesOnly .Params}} } + _returns := &{{.Name}}Returns{} + if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil { + g.log.Error("RPC call to {{.Name}} API failed.", mlog.Err(err)) + } + return {{destruct "_returns." .Return}} +} + +func (s *APIRPCServer) {{.Name}}(args *{{.Name}}Args, returns *{{.Name}}Returns) error { + if hook, ok := s.impl.(interface { + {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} + }); ok { + {{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}}) + } + return nil +} +{{end}} +` + +type MethodParams struct { + Name string + Params *ast.FieldList + Return *ast.FieldList +} + +type HooksTemplateParams struct { + HooksMethods []MethodParams + APIMethods []MethodParams +} + +func generateGlue(info *PluginInterfaceInfo) { + templateFunctions := map[string]interface{}{ + "funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) }, + "structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) }, + "valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, info.FileSet) }, + "destruct": func(structPrefix string, fields *ast.FieldList) string { + return FieldListDestruct(structPrefix, fields, info.FileSet) + }, + } + + hooksTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(hooksTemplate) + if err != nil { + panic(err) + } + + templateParams := HooksTemplateParams{} + for _, hook := range info.Hooks { + templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{ + Name: hook.FuncName, + Params: hook.Args, + Return: hook.Results, + }) + } + for _, api := range info.API { + templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{ + Name: api.FuncName, + Params: api.Args, + Return: api.Results, + }) + } + templateResult := &bytes.Buffer{} + hooksTemplate.Execute(templateResult, &templateParams) + + importsBuffer := &bytes.Buffer{} + cmd := exec.Command("goimports") + cmd.Stdin = templateResult + cmd.Stdout = importsBuffer + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + panic(err) + } + + if err := ioutil.WriteFile(filepath.Join(getPluginPackageDir(), "client_rpc_generated.go"), importsBuffer.Bytes(), 0664); err != nil { + panic(err) + } +} + +func getPluginPackageDir() string { + dirs, err := goList("github.com/mattermost/mattermost-server/plugin") + if err != nil { + panic(err) + } else if len(dirs) != 1 { + panic("More than one package dir, or no dirs!") + } + + return dirs[0] +} + +func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo { + toBeExcluded := func(item string) bool { + excluded := []string{ + "OnActivate", + "Implemented", + "LoadPluginConfiguration", + "ServeHTTP", + } + for _, exclusion := range excluded { + if exclusion == item { + return true + } + } + return false + } + hooksResult := make([]IHookEntry, 0, len(info.Hooks)) + for _, hook := range info.Hooks { + if !toBeExcluded(hook.FuncName) { + hooksResult = append(hooksResult, hook) + } + } + info.Hooks = hooksResult + + apiResult := make([]IHookEntry, 0, len(info.API)) + for _, api := range info.API { + if !toBeExcluded(api.FuncName) { + apiResult = append(apiResult, api) + } + } + info.API = apiResult + + return info +} + +func main() { + pluginPackageDir := getPluginPackageDir() + + log.Println("Generating plugin glue") + info, err := getPluginInfo(pluginPackageDir) + if err != nil { + fmt.Println("Unable to get plugin info: " + err.Error()) + } + + info = removeExcluded(info) + + generateGlue(info) +} diff --git a/plugin/io_rpc.go b/plugin/io_rpc.go new file mode 100644 index 000000000..7bb86b52b --- /dev/null +++ b/plugin/io_rpc.go @@ -0,0 +1,63 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin + +import ( + "bufio" + "encoding/binary" + "io" +) + +type rwc struct { + io.ReadCloser + io.WriteCloser +} + +func (rwc *rwc) Close() (err error) { + err = rwc.WriteCloser.Close() + if rerr := rwc.ReadCloser.Close(); err == nil { + err = rerr + } + return +} + +func NewReadWriteCloser(r io.ReadCloser, w io.WriteCloser) io.ReadWriteCloser { + return &rwc{r, w} +} + +type RemoteIOReader struct { + conn io.ReadWriteCloser +} + +func (r *RemoteIOReader) Read(b []byte) (int, error) { + var buf [10]byte + n := binary.PutVarint(buf[:], int64(len(b))) + if _, err := r.conn.Write(buf[:n]); err != nil { + return 0, err + } + return r.conn.Read(b) +} + +func (r *RemoteIOReader) Close() error { + return r.conn.Close() +} + +func ConnectIOReader(conn io.ReadWriteCloser) io.ReadCloser { + return &RemoteIOReader{conn} +} + +func ServeIOReader(r io.Reader, conn io.ReadWriteCloser) { + cr := bufio.NewReader(conn) + defer conn.Close() + buf := make([]byte, 32*1024) + for { + n, err := binary.ReadVarint(cr) + if err != nil { + break + } + if written, err := io.CopyBuffer(conn, io.LimitReader(r, n), buf); err != nil || written < n { + break + } + } +} diff --git a/plugin/plugin.go b/plugin/plugin.go deleted file mode 100644 index 3150bf56a..000000000 --- a/plugin/plugin.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -// The plugin package defines the primary interfaces for interacting with a Mattermost server: the -// API and the hook interfaces. -// -// The API interface is used to perform actions. The Hook interface is used to respond to actions. -// -// Plugins should define a type that implements some of the methods from the Hook interface, then -// pass an instance of that object into the rpcplugin package's Main function (See the HelloWorld -// example.). -// -// Testing -// -// To make testing plugins easier, you can use the plugintest package to create a mock API for your -// plugin to interact with. See -// https://godoc.org/github.com/mattermost/mattermost-server/plugin/plugintest -package plugin diff --git a/plugin/pluginenv/environment.go b/plugin/pluginenv/environment.go deleted file mode 100644 index f704aa5bb..000000000 --- a/plugin/pluginenv/environment.go +++ /dev/null @@ -1,396 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -// Package pluginenv provides high level functionality for discovering and launching plugins. -package pluginenv - -import ( - "fmt" - "io/ioutil" - "net/http" - "path/filepath" - "sync" - - "github.com/pkg/errors" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" -) - -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]ActivePlugin - mutex sync.RWMutex -} - -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]ActivePlugin), - } - for _, opt := range options { - opt(env) - } - if env.supervisorProvider == nil { - env.supervisorProvider = DefaultSupervisorProvider - } - if env.searchPath == "" { - return nil, fmt.Errorf("a search path 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() ([]*model.BundleInfo, error) { - return ScanSearchPath(env.searchPath) -} - -// Returns a list of all currently active plugins within the environment. -func (env *Environment) ActivePlugins() []*model.BundleInfo { - env.mutex.RLock() - defer env.mutex.RUnlock() - - activePlugins := []*model.BundleInfo{} - for _, p := range env.activePlugins { - activePlugins = append(activePlugins, p.BundleInfo) - } - - return activePlugins -} - -// Returns the ids of the currently active plugins. -func (env *Environment) ActivePluginIds() (ids []string) { - env.mutex.RLock() - defer env.mutex.RUnlock() - - for id := range env.activePlugins { - ids = append(ids, id) - } - return -} - -// Returns true if the plugin is active, false otherwise. -func (env *Environment) IsPluginActive(pluginId string) bool { - env.mutex.RLock() - defer env.mutex.RUnlock() - - for id := range env.activePlugins { - if id == pluginId { - return true - } - } - - return false -} - -// Activates the plugin with the given id. -func (env *Environment) ActivatePlugin(id string, onError func(error)) error { - env.mutex.Lock() - defer env.mutex.Unlock() - - if !plugin.IsValidId(id) { - return fmt.Errorf("invalid plugin id: %s", id) - } - - if _, ok := env.activePlugins[id]; ok { - return fmt.Errorf("plugin already active: %v", id) - } - plugins, err := ScanSearchPath(env.searchPath) - if err != nil { - return err - } - var bundle *model.BundleInfo - for _, p := range plugins { - if p.Manifest != nil && p.Manifest.Id == id { - if bundle != nil { - return fmt.Errorf("multiple plugins found: %v", id) - } - bundle = p - } - } - if bundle == nil { - return fmt.Errorf("plugin not found: %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(api); err != nil { - return errors.Wrapf(err, "unable to start plugin: %v", id) - } - if onError != nil { - go func() { - err := supervisor.Wait() - if err != nil { - onError(err) - } - }() - } - - activePlugin.Supervisor = supervisor - } - - 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) - } - - bundlePath := filepath.Clean(bundle.Manifest.Webapp.BundlePath) - if bundlePath == "" || bundlePath[0] == '.' { - return fmt.Errorf("invalid webapp bundle path") - } - bundlePath = filepath.Join(env.searchPath, id, bundlePath) - - webappBundle, err := ioutil.ReadFile(bundlePath) - if err != nil { - // Backwards compatibility for plugins where webapp.bundle_path was ignored. This should - // be removed eventually. - if webappBundle2, err2 := ioutil.ReadFile(fmt.Sprintf("%s/%s/webapp/%s_bundle.js", env.searchPath, id, id)); err2 == nil { - webappBundle = webappBundle2 - } else { - 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) - } - } - - env.activePlugins[id] = activePlugin - return nil -} - -// Deactivates the plugin with the given id. -func (env *Environment) DeactivatePlugin(id string) error { - 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) - var err error - if activePlugin.Supervisor != nil { - err = activePlugin.Supervisor.Hooks().OnDeactivate() - if serr := activePlugin.Supervisor.Stop(); err == nil { - err = serr - } - } - return err - } -} - -// Deactivates all plugins and gracefully shuts down the environment. -func (env *Environment) Shutdown() (errs []error) { - 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, errors.Wrapf(err, "OnDeactivate() error for %v", activePlugin.BundleInfo.Manifest.Id)) - } - if err := activePlugin.Supervisor.Stop(); err != nil { - errs = append(errs, errors.Wrapf(err, "error stopping supervisor for %v", activePlugin.BundleInfo.Manifest.Id)) - } - } - } - env.activePlugins = make(map[string]ActivePlugin) - return -} - -type MultiPluginHooks struct { - env *Environment -} - -type SinglePluginHooks struct { - env *Environment - pluginId string -} - -func (env *Environment) Hooks() *MultiPluginHooks { - return &MultiPluginHooks{ - env: env, - } -} - -func (env *Environment) HooksForPlugin(id string) *SinglePluginHooks { - return &SinglePluginHooks{ - env: env, - pluginId: id, - } -} - -func (h *MultiPluginHooks) invoke(f func(plugin.Hooks) error) (errs []error) { - h.env.mutex.RLock() - defer h.env.mutex.RUnlock() - - for _, activePlugin := range h.env.activePlugins { - if activePlugin.Supervisor == nil { - continue - } - if err := f(activePlugin.Supervisor.Hooks()); err != nil { - errs = append(errs, errors.Wrapf(err, "hook error for %v", activePlugin.BundleInfo.Manifest.Id)) - } - } - return -} - -// OnConfigurationChange invokes the OnConfigurationChange hook for all plugins. Any errors -// encountered will be returned. -func (h *MultiPluginHooks) OnConfigurationChange() []error { - return h.invoke(func(hooks plugin.Hooks) error { - if err := hooks.OnConfigurationChange(); err != nil { - return errors.Wrapf(err, "error calling OnConfigurationChange hook") - } - return nil - }) -} - -// ServeHTTP invokes the ServeHTTP hook for the plugin identified by the request or responds with a -// 404 not found. -// -// It expects the request's context to have a plugin_id set. -func (h *MultiPluginHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if id := r.Context().Value("plugin_id"); id != nil { - if idstr, ok := id.(string); ok { - h.env.mutex.RLock() - defer h.env.mutex.RUnlock() - if plugin, ok := h.env.activePlugins[idstr]; ok && plugin.Supervisor != nil { - plugin.Supervisor.Hooks().ServeHTTP(w, r) - return - } - } - } - http.NotFound(w, r) -} - -// MessageWillBePosted invokes the MessageWillBePosted hook for all plugins. Ordering -// is not guaranteed and the next plugin will get the previous one's modifications. -// if a plugin rejects a post, the rest of the plugins will not know that an attempt was made. -// Returns the final result post, or nil if the post was rejected and a string with a reason -// for the user the message was rejected. -func (h *MultiPluginHooks) MessageWillBePosted(post *model.Post) (*model.Post, string) { - h.env.mutex.RLock() - defer h.env.mutex.RUnlock() - - for _, activePlugin := range h.env.activePlugins { - if activePlugin.Supervisor == nil { - continue - } - var rejectionReason string - post, rejectionReason = activePlugin.Supervisor.Hooks().MessageWillBePosted(post) - if post == nil { - return nil, rejectionReason - } - } - return post, "" -} - -// MessageWillBeUpdated invokes the MessageWillBeUpdated hook for all plugins. Ordering -// is not guaranteed and the next plugin will get the previous one's modifications. -// if a plugin rejects a post, the rest of the plugins will not know that an attempt was made. -// Returns the final result post, or nil if the post was rejected and a string with a reason -// for the user the message was rejected. -func (h *MultiPluginHooks) MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string) { - h.env.mutex.RLock() - defer h.env.mutex.RUnlock() - - post := newPost - for _, activePlugin := range h.env.activePlugins { - if activePlugin.Supervisor == nil { - continue - } - var rejectionReason string - post, rejectionReason = activePlugin.Supervisor.Hooks().MessageWillBeUpdated(post, oldPost) - if post == nil { - return nil, rejectionReason - } - } - return post, "" -} - -func (h *MultiPluginHooks) MessageHasBeenPosted(post *model.Post) { - h.invoke(func(hooks plugin.Hooks) error { - hooks.MessageHasBeenPosted(post) - return nil - }) -} - -func (h *MultiPluginHooks) MessageHasBeenUpdated(newPost, oldPost *model.Post) { - h.invoke(func(hooks plugin.Hooks) error { - hooks.MessageHasBeenUpdated(newPost, oldPost) - return nil - }) -} - -func (h *SinglePluginHooks) invoke(f func(plugin.Hooks) error) error { - h.env.mutex.RLock() - defer h.env.mutex.RUnlock() - - if activePlugin, ok := h.env.activePlugins[h.pluginId]; ok && activePlugin.Supervisor != nil { - if err := f(activePlugin.Supervisor.Hooks()); err != nil { - return errors.Wrapf(err, "hook error for plugin: %v", activePlugin.BundleInfo.Manifest.Id) - } - return nil - } - return fmt.Errorf("unable to invoke hook for plugin: %v", h.pluginId) -} - -// ExecuteCommand invokes the ExecuteCommand hook for the plugin. -func (h *SinglePluginHooks) ExecuteCommand(args *model.CommandArgs) (resp *model.CommandResponse, appErr *model.AppError, err error) { - err = h.invoke(func(hooks plugin.Hooks) error { - resp, appErr = hooks.ExecuteCommand(args) - return nil - }) - return -} diff --git a/plugin/pluginenv/environment_test.go b/plugin/pluginenv/environment_test.go deleted file mode 100644 index 8c1397799..000000000 --- a/plugin/pluginenv/environment_test.go +++ /dev/null @@ -1,409 +0,0 @@ -package pluginenv - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/plugin/plugintest" -) - -type MockProvider struct { - mock.Mock -} - -func (m *MockProvider) API(manifest *model.Manifest) (plugin.API, error) { - ret := m.Called() - if ret.Get(0) == nil { - return nil, ret.Error(1) - } - return ret.Get(0).(plugin.API), ret.Error(1) -} - -func (m *MockProvider) Supervisor(bundle *model.BundleInfo) (plugin.Supervisor, error) { - ret := m.Called() - if ret.Get(0) == nil { - return nil, ret.Error(1) - } - return ret.Get(0).(plugin.Supervisor), ret.Error(1) -} - -type MockSupervisor struct { - mock.Mock -} - -func (m *MockSupervisor) Start(api plugin.API) error { - return m.Called(api).Error(0) -} - -func (m *MockSupervisor) Stop() error { - return m.Called().Error(0) -} - -func (m *MockSupervisor) Hooks() plugin.Hooks { - return m.Called().Get(0).(plugin.Hooks) -} - -func (m *MockSupervisor) Wait() error { - return m.Called().Get(0).(error) -} - -func initTmpDir(t *testing.T, files map[string]string) string { - success := false - dir, err := ioutil.TempDir("", "mm-plugin-test") - require.NoError(t, err) - defer func() { - if !success { - os.RemoveAll(dir) - } - }() - - for name, contents := range files { - path := filepath.Join(dir, name) - parent := filepath.Dir(path) - require.NoError(t, os.MkdirAll(parent, 0700)) - f, err := os.Create(path) - require.NoError(t, err) - _, err = f.WriteString(contents) - f.Close() - require.NoError(t, err) - } - - success = true - return dir -} - -func TestNew_MissingOptions(t *testing.T) { - dir := initTmpDir(t, map[string]string{ - "foo/plugin.json": `{"id": "foo"}`, - }) - defer os.RemoveAll(dir) - - var provider MockProvider - defer provider.AssertExpectations(t) - - env, err := New( - APIProvider(provider.API), - ) - 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", "backend": {}}`, - "bar/zxc": "qwer", - "baz/plugin.yaml": "id: baz", - "bad/plugin.json": "asd", - "qwe": "asd", - }) - 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), - ) - require.NoError(t, err) - defer env.Shutdown() - - plugins, err := env.Plugins() - assert.NoError(t, err) - assert.Len(t, plugins, 3) - - activePlugins := env.ActivePlugins() - assert.Len(t, activePlugins, 0) - - assert.Error(t, env.ActivatePlugin("x", nil)) - - var api struct{ plugin.API } - var supervisor MockSupervisor - defer supervisor.AssertExpectations(t) - var hooks plugintest.Hooks - defer hooks.AssertExpectations(t) - - provider.On("API").Return(&api, nil) - provider.On("Supervisor").Return(&supervisor, nil) - - supervisor.On("Start", &api).Return(nil) - supervisor.On("Stop").Return(nil) - supervisor.On("Hooks").Return(&hooks) - - assert.NoError(t, env.ActivatePlugin("foo", nil)) - assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) - activePlugins = env.ActivePlugins() - assert.Len(t, activePlugins, 1) - assert.Error(t, env.ActivatePlugin("foo", nil)) - assert.True(t, env.IsPluginActive("foo")) - - hooks.On("OnDeactivate").Return(nil) - assert.NoError(t, env.DeactivatePlugin("foo")) - assert.Error(t, env.DeactivatePlugin("foo")) - assert.False(t, env.IsPluginActive("foo")) - - assert.NoError(t, env.ActivatePlugin("foo", nil)) - assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) - - assert.Equal(t, env.SearchPath(), dir) - assert.Equal(t, env.WebappPath(), webappDir) - - assert.Empty(t, env.Shutdown()) -} - -func TestEnvironment_DuplicatePluginError(t *testing.T) { - dir := initTmpDir(t, map[string]string{ - "foo/plugin.json": `{"id": "foo"}`, - "foo2/plugin.json": `{"id": "foo"}`, - }) - defer os.RemoveAll(dir) - - var provider MockProvider - defer provider.AssertExpectations(t) - - env, err := New( - SearchPath(dir), - APIProvider(provider.API), - SupervisorProvider(provider.Supervisor), - ) - require.NoError(t, err) - defer env.Shutdown() - - assert.Error(t, env.ActivatePlugin("foo", nil)) - assert.Empty(t, env.ActivePluginIds()) -} - -func TestEnvironment_BadSearchPathError(t *testing.T) { - var provider MockProvider - defer provider.AssertExpectations(t) - - env, err := New( - SearchPath("thissearchpathshouldnotexist!"), - APIProvider(provider.API), - SupervisorProvider(provider.Supervisor), - ) - require.NoError(t, err) - defer env.Shutdown() - - assert.Error(t, env.ActivatePlugin("foo", nil)) - assert.Empty(t, env.ActivePluginIds()) -} - -func TestEnvironment_ActivatePluginErrors(t *testing.T) { - dir := initTmpDir(t, map[string]string{ - "foo/plugin.json": `{"id": "foo", "backend": {}}`, - }) - defer os.RemoveAll(dir) - - var provider MockProvider - - env, err := New( - SearchPath(dir), - APIProvider(provider.API), - SupervisorProvider(provider.Supervisor), - ) - require.NoError(t, err) - defer env.Shutdown() - - var api struct{ plugin.API } - var supervisor MockSupervisor - var hooks plugintest.Hooks - - for name, setup := range map[string]func(){ - "SupervisorProviderError": func() { - provider.On("Supervisor").Return(nil, fmt.Errorf("test error")) - }, - "APIProviderError": func() { - provider.On("API").Return(plugin.API(nil), fmt.Errorf("test error")) - provider.On("Supervisor").Return(&supervisor, nil) - }, - "SupervisorError": func() { - provider.On("API").Return(&api, nil) - provider.On("Supervisor").Return(&supervisor, nil) - - supervisor.On("Start", &api).Return(fmt.Errorf("test error")) - }, - } { - t.Run(name, func(t *testing.T) { - supervisor.Mock = mock.Mock{} - hooks.Mock = mock.Mock{} - provider.Mock = mock.Mock{} - setup() - assert.Error(t, env.ActivatePlugin("foo", nil)) - assert.Empty(t, env.ActivePluginIds()) - supervisor.AssertExpectations(t) - hooks.AssertExpectations(t) - provider.AssertExpectations(t) - }) - } -} - -func TestEnvironment_ShutdownError(t *testing.T) { - dir := initTmpDir(t, map[string]string{ - "foo/plugin.json": `{"id": "foo", "backend": {}}`, - }) - defer os.RemoveAll(dir) - - var provider MockProvider - defer provider.AssertExpectations(t) - - env, err := New( - SearchPath(dir), - APIProvider(provider.API), - SupervisorProvider(provider.Supervisor), - ) - require.NoError(t, err) - defer env.Shutdown() - - var api struct{ plugin.API } - var supervisor MockSupervisor - defer supervisor.AssertExpectations(t) - var hooks plugintest.Hooks - defer hooks.AssertExpectations(t) - - provider.On("API").Return(&api, nil) - provider.On("Supervisor").Return(&supervisor, nil) - - supervisor.On("Start", &api).Return(nil) - supervisor.On("Stop").Return(fmt.Errorf("test error")) - supervisor.On("Hooks").Return(&hooks) - - hooks.On("OnDeactivate").Return(fmt.Errorf("test error")) - - assert.NoError(t, env.ActivatePlugin("foo", nil)) - assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) - assert.Len(t, env.Shutdown(), 2) -} - -func TestEnvironment_ConcurrentHookInvocations(t *testing.T) { - dir := initTmpDir(t, map[string]string{ - "foo/plugin.json": `{"id": "foo", "backend": {}}`, - }) - defer os.RemoveAll(dir) - - var provider MockProvider - defer provider.AssertExpectations(t) - - var api struct{ plugin.API } - var supervisor MockSupervisor - defer supervisor.AssertExpectations(t) - var hooks plugintest.Hooks - defer hooks.AssertExpectations(t) - - env, err := New( - SearchPath(dir), - APIProvider(provider.API), - SupervisorProvider(provider.Supervisor), - ) - require.NoError(t, err) - defer env.Shutdown() - - provider.On("API").Return(&api, nil) - provider.On("Supervisor").Return(&supervisor, nil) - - supervisor.On("Start", &api).Return(nil) - supervisor.On("Stop").Return(nil) - supervisor.On("Hooks").Return(&hooks) - - ch := make(chan bool) - - hooks.On("OnDeactivate").Return(nil) - hooks.On("ServeHTTP", mock.AnythingOfType("*httptest.ResponseRecorder"), mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) { - r := args.Get(1).(*http.Request) - if r.URL.Path == "/1" { - <-ch - } else { - ch <- true - } - }) - - assert.NoError(t, env.ActivatePlugin("foo", nil)) - - rec := httptest.NewRecorder() - - wg := sync.WaitGroup{} - wg.Add(2) - - go func() { - req, err := http.NewRequest("GET", "/1", nil) - require.NoError(t, err) - env.Hooks().ServeHTTP(rec, req.WithContext(context.WithValue(context.Background(), "plugin_id", "foo"))) - wg.Done() - }() - - go func() { - req, err := http.NewRequest("GET", "/2", nil) - require.NoError(t, err) - env.Hooks().ServeHTTP(rec, req.WithContext(context.WithValue(context.Background(), "plugin_id", "foo"))) - wg.Done() - }() - - wg.Wait() -} - -func TestEnvironment_HooksForPlugins(t *testing.T) { - dir := initTmpDir(t, map[string]string{ - "foo/plugin.json": `{"id": "foo", "backend": {}}`, - }) - defer os.RemoveAll(dir) - - var provider MockProvider - defer provider.AssertExpectations(t) - - env, err := New( - SearchPath(dir), - APIProvider(provider.API), - SupervisorProvider(provider.Supervisor), - ) - require.NoError(t, err) - defer env.Shutdown() - - var api struct{ plugin.API } - var supervisor MockSupervisor - defer supervisor.AssertExpectations(t) - var hooks plugintest.Hooks - defer hooks.AssertExpectations(t) - - provider.On("API").Return(&api, nil) - provider.On("Supervisor").Return(&supervisor, nil) - - supervisor.On("Start", &api).Return(nil) - supervisor.On("Stop").Return(nil) - supervisor.On("Hooks").Return(&hooks) - - hooks.On("OnDeactivate").Return(nil) - hooks.On("ExecuteCommand", mock.AnythingOfType("*model.CommandArgs")).Return(&model.CommandResponse{ - Text: "bar", - }, nil) - - assert.NoError(t, env.ActivatePlugin("foo", nil)) - assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) - - resp, appErr, err := env.HooksForPlugin("foo").ExecuteCommand(&model.CommandArgs{ - Command: "/foo", - }) - assert.Equal(t, "bar", resp.Text) - assert.Nil(t, appErr) - assert.NoError(t, err) - - assert.Empty(t, env.Shutdown()) -} diff --git a/plugin/pluginenv/options.go b/plugin/pluginenv/options.go deleted file mode 100644 index 43cbdac68..000000000 --- a/plugin/pluginenv/options.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package pluginenv - -import ( - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/plugin/rpcplugin" - "github.com/mattermost/mattermost-server/plugin/rpcplugin/sandbox" -) - -// APIProvider specifies a function that provides an API implementation to each plugin. -func APIProvider(provider APIProviderFunc) Option { - return func(env *Environment) { - env.apiProvider = provider - } -} - -// SupervisorProvider specifies a function that provides a Supervisor implementation to each plugin. -// If unspecified, DefaultSupervisorProvider is used. -func SupervisorProvider(provider SupervisorProviderFunc) Option { - return func(env *Environment) { - env.supervisorProvider = provider - } -} - -// SearchPath specifies a directory that contains the plugins to launch. -func SearchPath(path string) Option { - return func(env *Environment) { - env.searchPath = path - } -} - -// 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 system and the plugin's manifest -// contents. E.g. if the manifest specifies a backend executable, it will be given an -// rpcplugin.Supervisor. -func DefaultSupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) { - if err := sandbox.CheckSupport(); err == nil { - return sandbox.SupervisorProvider(bundle) - } - return rpcplugin.SupervisorProvider(bundle) -} diff --git a/plugin/pluginenv/options_test.go b/plugin/pluginenv/options_test.go deleted file mode 100644 index 01fa206d0..000000000 --- a/plugin/pluginenv/options_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package pluginenv - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin/rpcplugin" -) - -func TestDefaultSupervisorProvider(t *testing.T) { - _, err := DefaultSupervisorProvider(&model.BundleInfo{}) - assert.Error(t, err) - - _, err = DefaultSupervisorProvider(&model.BundleInfo{ - Manifest: &model.Manifest{}, - }) - assert.Error(t, err) - - supervisor, err := DefaultSupervisorProvider(&model.BundleInfo{ - Manifest: &model.Manifest{ - Backend: &model.ManifestBackend{ - Executable: "foo", - }, - }, - }) - require.NoError(t, err) - _, ok := supervisor.(*rpcplugin.Supervisor) - assert.True(t, ok) -} diff --git a/plugin/pluginenv/search_path.go b/plugin/pluginenv/search_path.go deleted file mode 100644 index 698424332..000000000 --- a/plugin/pluginenv/search_path.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package pluginenv - -import ( - "io/ioutil" - "path/filepath" - - "github.com/mattermost/mattermost-server/model" -) - -// Performs a full scan of the given path. -// -// This function will return info for all subdirectories that appear to be plugins (i.e. all -// subdirectories containing plugin manifest files, regardless of whether they could actually be -// parsed). -// -// Plugins are found non-recursively and paths beginning with a dot are always ignored. -func ScanSearchPath(path string) ([]*model.BundleInfo, error) { - files, err := ioutil.ReadDir(path) - if err != nil { - return nil, err - } - var ret []*model.BundleInfo - for _, file := range files { - if !file.IsDir() || file.Name()[0] == '.' { - continue - } - if info := model.BundleInfoForPath(filepath.Join(path, file.Name())); info.ManifestPath != "" { - ret = append(ret, info) - } - } - return ret, nil -} diff --git a/plugin/pluginenv/search_path_test.go b/plugin/pluginenv/search_path_test.go deleted file mode 100644 index dd9ff9a6b..000000000 --- a/plugin/pluginenv/search_path_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package pluginenv - -import ( - "encoding/json" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/model" -) - -func TestScanSearchPath(t *testing.T) { - dir := initTmpDir(t, map[string]string{ - ".foo/plugin.json": `{"id": "foo"}`, - "foo/bar": "asdf", - "foo/plugin.json": `{"id": "foo"}`, - "bar/zxc": "qwer", - "baz/plugin.yaml": "id: baz", - "bad/plugin.json": "asd", - "qwe": "asd", - }) - defer os.RemoveAll(dir) - - plugins, err := ScanSearchPath(dir) - require.NoError(t, err) - assert.Len(t, plugins, 3) - assert.Contains(t, plugins, &model.BundleInfo{ - Path: filepath.Join(dir, "foo"), - ManifestPath: filepath.Join(dir, "foo", "plugin.json"), - Manifest: &model.Manifest{ - Id: "foo", - }, - }) - assert.Contains(t, plugins, &model.BundleInfo{ - Path: filepath.Join(dir, "baz"), - ManifestPath: filepath.Join(dir, "baz", "plugin.yaml"), - Manifest: &model.Manifest{ - Id: "baz", - }, - }) - foundError := false - for _, x := range plugins { - if x.ManifestError != nil { - assert.Equal(t, x.Path, filepath.Join(dir, "bad")) - assert.Equal(t, x.ManifestPath, filepath.Join(dir, "bad", "plugin.json")) - syntexError, ok := x.ManifestError.(*json.SyntaxError) - assert.True(t, ok) - assert.EqualValues(t, 1, syntexError.Offset) - foundError = true - } - } - assert.True(t, foundError) -} - -func TestScanSearchPath_Error(t *testing.T) { - plugins, err := ScanSearchPath("not a valid path!") - assert.Nil(t, plugins) - assert.Error(t, err) -} diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index f1281a5ff..11c5a9c59 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -6,15 +6,14 @@ package plugintest import mock "github.com/stretchr/testify/mock" import model "github.com/mattermost/mattermost-server/model" -import plugin "github.com/mattermost/mattermost-server/plugin" -// APIMOCKINTERNAL is an autogenerated mock type for the APIMOCKINTERNAL type -type APIMOCKINTERNAL struct { +// API is an autogenerated mock type for the API type +type API struct { mock.Mock } // AddChannelMember provides a mock function with given fields: channelId, userId -func (_m *APIMOCKINTERNAL) AddChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) { +func (_m *API) AddChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) { ret := _m.Called(channelId, userId) var r0 *model.ChannelMember @@ -39,7 +38,7 @@ func (_m *APIMOCKINTERNAL) AddChannelMember(channelId string, userId string) (*m } // CreateChannel provides a mock function with given fields: channel -func (_m *APIMOCKINTERNAL) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { +func (_m *API) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { ret := _m.Called(channel) var r0 *model.Channel @@ -64,7 +63,7 @@ func (_m *APIMOCKINTERNAL) CreateChannel(channel *model.Channel) (*model.Channel } // CreatePost provides a mock function with given fields: post -func (_m *APIMOCKINTERNAL) CreatePost(post *model.Post) (*model.Post, *model.AppError) { +func (_m *API) CreatePost(post *model.Post) (*model.Post, *model.AppError) { ret := _m.Called(post) var r0 *model.Post @@ -89,7 +88,7 @@ func (_m *APIMOCKINTERNAL) CreatePost(post *model.Post) (*model.Post, *model.App } // CreateTeam provides a mock function with given fields: team -func (_m *APIMOCKINTERNAL) CreateTeam(team *model.Team) (*model.Team, *model.AppError) { +func (_m *API) CreateTeam(team *model.Team) (*model.Team, *model.AppError) { ret := _m.Called(team) var r0 *model.Team @@ -114,7 +113,7 @@ func (_m *APIMOCKINTERNAL) CreateTeam(team *model.Team) (*model.Team, *model.App } // CreateUser provides a mock function with given fields: user -func (_m *APIMOCKINTERNAL) CreateUser(user *model.User) (*model.User, *model.AppError) { +func (_m *API) CreateUser(user *model.User) (*model.User, *model.AppError) { ret := _m.Called(user) var r0 *model.User @@ -139,7 +138,7 @@ func (_m *APIMOCKINTERNAL) CreateUser(user *model.User) (*model.User, *model.App } // DeleteChannel provides a mock function with given fields: channelId -func (_m *APIMOCKINTERNAL) DeleteChannel(channelId string) *model.AppError { +func (_m *API) DeleteChannel(channelId string) *model.AppError { ret := _m.Called(channelId) var r0 *model.AppError @@ -155,7 +154,7 @@ func (_m *APIMOCKINTERNAL) DeleteChannel(channelId string) *model.AppError { } // DeleteChannelMember provides a mock function with given fields: channelId, userId -func (_m *APIMOCKINTERNAL) DeleteChannelMember(channelId string, userId string) *model.AppError { +func (_m *API) DeleteChannelMember(channelId string, userId string) *model.AppError { ret := _m.Called(channelId, userId) var r0 *model.AppError @@ -171,7 +170,7 @@ func (_m *APIMOCKINTERNAL) DeleteChannelMember(channelId string, userId string) } // DeletePost provides a mock function with given fields: postId -func (_m *APIMOCKINTERNAL) DeletePost(postId string) *model.AppError { +func (_m *API) DeletePost(postId string) *model.AppError { ret := _m.Called(postId) var r0 *model.AppError @@ -187,7 +186,7 @@ func (_m *APIMOCKINTERNAL) DeletePost(postId string) *model.AppError { } // DeleteTeam provides a mock function with given fields: teamId -func (_m *APIMOCKINTERNAL) DeleteTeam(teamId string) *model.AppError { +func (_m *API) DeleteTeam(teamId string) *model.AppError { ret := _m.Called(teamId) var r0 *model.AppError @@ -203,7 +202,7 @@ func (_m *APIMOCKINTERNAL) DeleteTeam(teamId string) *model.AppError { } // DeleteUser provides a mock function with given fields: userId -func (_m *APIMOCKINTERNAL) DeleteUser(userId string) *model.AppError { +func (_m *API) DeleteUser(userId string) *model.AppError { ret := _m.Called(userId) var r0 *model.AppError @@ -219,7 +218,7 @@ func (_m *APIMOCKINTERNAL) DeleteUser(userId string) *model.AppError { } // GetChannel provides a mock function with given fields: channelId -func (_m *APIMOCKINTERNAL) GetChannel(channelId string) (*model.Channel, *model.AppError) { +func (_m *API) GetChannel(channelId string) (*model.Channel, *model.AppError) { ret := _m.Called(channelId) var r0 *model.Channel @@ -244,7 +243,7 @@ func (_m *APIMOCKINTERNAL) GetChannel(channelId string) (*model.Channel, *model. } // GetChannelByName provides a mock function with given fields: name, teamId -func (_m *APIMOCKINTERNAL) GetChannelByName(name string, teamId string) (*model.Channel, *model.AppError) { +func (_m *API) GetChannelByName(name string, teamId string) (*model.Channel, *model.AppError) { ret := _m.Called(name, teamId) var r0 *model.Channel @@ -269,7 +268,7 @@ func (_m *APIMOCKINTERNAL) GetChannelByName(name string, teamId string) (*model. } // GetChannelMember provides a mock function with given fields: channelId, userId -func (_m *APIMOCKINTERNAL) GetChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) { +func (_m *API) GetChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) { ret := _m.Called(channelId, userId) var r0 *model.ChannelMember @@ -294,7 +293,7 @@ func (_m *APIMOCKINTERNAL) GetChannelMember(channelId string, userId string) (*m } // GetDirectChannel provides a mock function with given fields: userId1, userId2 -func (_m *APIMOCKINTERNAL) GetDirectChannel(userId1 string, userId2 string) (*model.Channel, *model.AppError) { +func (_m *API) GetDirectChannel(userId1 string, userId2 string) (*model.Channel, *model.AppError) { ret := _m.Called(userId1, userId2) var r0 *model.Channel @@ -319,7 +318,7 @@ func (_m *APIMOCKINTERNAL) GetDirectChannel(userId1 string, userId2 string) (*mo } // GetGroupChannel provides a mock function with given fields: userIds -func (_m *APIMOCKINTERNAL) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) { +func (_m *API) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) { ret := _m.Called(userIds) var r0 *model.Channel @@ -344,7 +343,7 @@ func (_m *APIMOCKINTERNAL) GetGroupChannel(userIds []string) (*model.Channel, *m } // GetPost provides a mock function with given fields: postId -func (_m *APIMOCKINTERNAL) GetPost(postId string) (*model.Post, *model.AppError) { +func (_m *API) GetPost(postId string) (*model.Post, *model.AppError) { ret := _m.Called(postId) var r0 *model.Post @@ -369,7 +368,7 @@ func (_m *APIMOCKINTERNAL) GetPost(postId string) (*model.Post, *model.AppError) } // GetTeam provides a mock function with given fields: teamId -func (_m *APIMOCKINTERNAL) GetTeam(teamId string) (*model.Team, *model.AppError) { +func (_m *API) GetTeam(teamId string) (*model.Team, *model.AppError) { ret := _m.Called(teamId) var r0 *model.Team @@ -394,7 +393,7 @@ func (_m *APIMOCKINTERNAL) GetTeam(teamId string) (*model.Team, *model.AppError) } // GetTeamByName provides a mock function with given fields: name -func (_m *APIMOCKINTERNAL) GetTeamByName(name string) (*model.Team, *model.AppError) { +func (_m *API) GetTeamByName(name string) (*model.Team, *model.AppError) { ret := _m.Called(name) var r0 *model.Team @@ -419,7 +418,7 @@ func (_m *APIMOCKINTERNAL) GetTeamByName(name string) (*model.Team, *model.AppEr } // GetUser provides a mock function with given fields: userId -func (_m *APIMOCKINTERNAL) GetUser(userId string) (*model.User, *model.AppError) { +func (_m *API) GetUser(userId string) (*model.User, *model.AppError) { ret := _m.Called(userId) var r0 *model.User @@ -444,7 +443,7 @@ func (_m *APIMOCKINTERNAL) GetUser(userId string) (*model.User, *model.AppError) } // GetUserByEmail provides a mock function with given fields: email -func (_m *APIMOCKINTERNAL) GetUserByEmail(email string) (*model.User, *model.AppError) { +func (_m *API) GetUserByEmail(email string) (*model.User, *model.AppError) { ret := _m.Called(email) var r0 *model.User @@ -469,7 +468,7 @@ func (_m *APIMOCKINTERNAL) GetUserByEmail(email string) (*model.User, *model.App } // GetUserByUsername provides a mock function with given fields: name -func (_m *APIMOCKINTERNAL) GetUserByUsername(name string) (*model.User, *model.AppError) { +func (_m *API) GetUserByUsername(name string) (*model.User, *model.AppError) { ret := _m.Called(name) var r0 *model.User @@ -493,16 +492,57 @@ func (_m *APIMOCKINTERNAL) GetUserByUsername(name string) (*model.User, *model.A return r0, r1 } -// KeyValueStore provides a mock function with given fields: -func (_m *APIMOCKINTERNAL) KeyValueStore() plugin.KeyValueStore { - ret := _m.Called() +// KVDelete provides a mock function with given fields: key +func (_m *API) KVDelete(key string) *model.AppError { + ret := _m.Called(key) - var r0 plugin.KeyValueStore - if rf, ok := ret.Get(0).(func() plugin.KeyValueStore); ok { - r0 = rf() + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string) *model.AppError); ok { + r0 = rf(key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + +// KVGet provides a mock function with given fields: key +func (_m *API) KVGet(key string) ([]byte, *model.AppError) { + ret := _m.Called(key) + + var r0 []byte + if rf, ok := ret.Get(0).(func(string) []byte); ok { + r0 = rf(key) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(plugin.KeyValueStore) + r0 = ret.Get(0).([]byte) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(key) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// KVSet provides a mock function with given fields: key, value +func (_m *API) KVSet(key string, value []byte) *model.AppError { + ret := _m.Called(key, value) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string, []byte) *model.AppError); ok { + r0 = rf(key, value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) } } @@ -510,7 +550,7 @@ func (_m *APIMOCKINTERNAL) KeyValueStore() plugin.KeyValueStore { } // LoadPluginConfiguration provides a mock function with given fields: dest -func (_m *APIMOCKINTERNAL) LoadPluginConfiguration(dest interface{}) error { +func (_m *API) LoadPluginConfiguration(dest interface{}) error { ret := _m.Called(dest) var r0 error @@ -524,7 +564,7 @@ func (_m *APIMOCKINTERNAL) LoadPluginConfiguration(dest interface{}) error { } // RegisterCommand provides a mock function with given fields: command -func (_m *APIMOCKINTERNAL) RegisterCommand(command *model.Command) error { +func (_m *API) RegisterCommand(command *model.Command) error { ret := _m.Called(command) var r0 error @@ -538,7 +578,7 @@ func (_m *APIMOCKINTERNAL) RegisterCommand(command *model.Command) error { } // UnregisterCommand provides a mock function with given fields: teamId, trigger -func (_m *APIMOCKINTERNAL) UnregisterCommand(teamId string, trigger string) error { +func (_m *API) UnregisterCommand(teamId string, trigger string) error { ret := _m.Called(teamId, trigger) var r0 error @@ -552,7 +592,7 @@ func (_m *APIMOCKINTERNAL) UnregisterCommand(teamId string, trigger string) erro } // UpdateChannel provides a mock function with given fields: channel -func (_m *APIMOCKINTERNAL) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { +func (_m *API) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { ret := _m.Called(channel) var r0 *model.Channel @@ -577,7 +617,7 @@ func (_m *APIMOCKINTERNAL) UpdateChannel(channel *model.Channel) (*model.Channel } // UpdateChannelMemberNotifications provides a mock function with given fields: channelId, userId, notifications -func (_m *APIMOCKINTERNAL) UpdateChannelMemberNotifications(channelId string, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) { +func (_m *API) UpdateChannelMemberNotifications(channelId string, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) { ret := _m.Called(channelId, userId, notifications) var r0 *model.ChannelMember @@ -602,7 +642,7 @@ func (_m *APIMOCKINTERNAL) UpdateChannelMemberNotifications(channelId string, us } // UpdateChannelMemberRoles provides a mock function with given fields: channelId, userId, newRoles -func (_m *APIMOCKINTERNAL) UpdateChannelMemberRoles(channelId string, userId string, newRoles string) (*model.ChannelMember, *model.AppError) { +func (_m *API) UpdateChannelMemberRoles(channelId string, userId string, newRoles string) (*model.ChannelMember, *model.AppError) { ret := _m.Called(channelId, userId, newRoles) var r0 *model.ChannelMember @@ -627,7 +667,7 @@ func (_m *APIMOCKINTERNAL) UpdateChannelMemberRoles(channelId string, userId str } // UpdatePost provides a mock function with given fields: post -func (_m *APIMOCKINTERNAL) UpdatePost(post *model.Post) (*model.Post, *model.AppError) { +func (_m *API) UpdatePost(post *model.Post) (*model.Post, *model.AppError) { ret := _m.Called(post) var r0 *model.Post @@ -652,7 +692,7 @@ func (_m *APIMOCKINTERNAL) UpdatePost(post *model.Post) (*model.Post, *model.App } // UpdateTeam provides a mock function with given fields: team -func (_m *APIMOCKINTERNAL) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) { +func (_m *API) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) { ret := _m.Called(team) var r0 *model.Team @@ -677,7 +717,7 @@ func (_m *APIMOCKINTERNAL) UpdateTeam(team *model.Team) (*model.Team, *model.App } // UpdateUser provides a mock function with given fields: user -func (_m *APIMOCKINTERNAL) UpdateUser(user *model.User) (*model.User, *model.AppError) { +func (_m *API) UpdateUser(user *model.User) (*model.User, *model.AppError) { ret := _m.Called(user) var r0 *model.User diff --git a/plugin/plugintest/apioverride.go b/plugin/plugintest/apioverride.go deleted file mode 100644 index 54cfe27bc..000000000 --- a/plugin/plugintest/apioverride.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package plugintest - -import "github.com/mattermost/mattermost-server/plugin" - -type API struct { - APIMOCKINTERNAL - Store *KeyValueStore -} - -var _ plugin.API = (*API)(nil) -var _ plugin.KeyValueStore = (*KeyValueStore)(nil) - -func (m *API) KeyValueStore() plugin.KeyValueStore { - return m.Store -} diff --git a/plugin/plugintest/hooks.go b/plugin/plugintest/hooks.go index 3de257c76..790a5a993 100644 --- a/plugin/plugintest/hooks.go +++ b/plugin/plugintest/hooks.go @@ -7,7 +7,6 @@ package plugintest import http "net/http" import mock "github.com/stretchr/testify/mock" import model "github.com/mattermost/mattermost-server/model" -import plugin "github.com/mattermost/mattermost-server/plugin" // Hooks is an autogenerated mock type for the Hooks type type Hooks struct { @@ -39,6 +38,29 @@ func (_m *Hooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse return r0, r1 } +// Implemented provides a mock function with given fields: +func (_m *Hooks) Implemented() ([]string, error) { + ret := _m.Called() + + var r0 []string + if rf, ok := ret.Get(0).(func() []string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // MessageHasBeenPosted provides a mock function with given fields: post func (_m *Hooks) MessageHasBeenPosted(post *model.Post) { _m.Called(post) @@ -95,13 +117,13 @@ func (_m *Hooks) MessageWillBeUpdated(newPost *model.Post, oldPost *model.Post) return r0, r1 } -// OnActivate provides a mock function with given fields: _a0 -func (_m *Hooks) OnActivate(_a0 plugin.API) error { - ret := _m.Called(_a0) +// OnActivate provides a mock function with given fields: +func (_m *Hooks) OnActivate() error { + ret := _m.Called() var r0 error - if rf, ok := ret.Get(0).(func(plugin.API) error); ok { - r0 = rf(_a0) + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() } else { r0 = ret.Error(0) } @@ -137,7 +159,7 @@ func (_m *Hooks) OnDeactivate() error { return r0 } -// ServeHTTP provides a mock function with given fields: _a0, _a1 -func (_m *Hooks) ServeHTTP(_a0 http.ResponseWriter, _a1 *http.Request) { - _m.Called(_a0, _a1) +// ServeHTTP provides a mock function with given fields: w, r +func (_m *Hooks) ServeHTTP(w http.ResponseWriter, r *http.Request) { + _m.Called(w, r) } diff --git a/plugin/plugintest/key_value_store.go b/plugin/plugintest/key_value_store.go deleted file mode 100644 index 30d60d708..000000000 --- a/plugin/plugintest/key_value_store.go +++ /dev/null @@ -1,70 +0,0 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. - -// Regenerate this file using `make plugin-mocks`. - -package plugintest - -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" - -// KeyValueStore is an autogenerated mock type for the KeyValueStore type -type KeyValueStore struct { - mock.Mock -} - -// Delete provides a mock function with given fields: key -func (_m *KeyValueStore) Delete(key string) *model.AppError { - ret := _m.Called(key) - - var r0 *model.AppError - if rf, ok := ret.Get(0).(func(string) *model.AppError); ok { - r0 = rf(key) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.AppError) - } - } - - return r0 -} - -// Get provides a mock function with given fields: key -func (_m *KeyValueStore) Get(key string) ([]byte, *model.AppError) { - ret := _m.Called(key) - - var r0 []byte - if rf, ok := ret.Get(0).(func(string) []byte); ok { - r0 = rf(key) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - var r1 *model.AppError - if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { - r1 = rf(key) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*model.AppError) - } - } - - return r0, r1 -} - -// Set provides a mock function with given fields: key, value -func (_m *KeyValueStore) Set(key string, value []byte) *model.AppError { - ret := _m.Called(key, value) - - var r0 *model.AppError - if rf, ok := ret.Get(0).(func(string, []byte) *model.AppError); ok { - r0 = rf(key, value) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*model.AppError) - } - } - - return r0 -} diff --git a/plugin/rpcplugin/api.go b/plugin/rpcplugin/api.go deleted file mode 100644 index c81bbb7c5..000000000 --- a/plugin/rpcplugin/api.go +++ /dev/null @@ -1,718 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "encoding/gob" - "encoding/json" - "io" - "net/http" - "net/rpc" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" -) - -type LocalAPI struct { - api plugin.API - muxer *Muxer -} - -func (api *LocalAPI) LoadPluginConfiguration(args struct{}, reply *[]byte) error { - var config interface{} - if err := api.api.LoadPluginConfiguration(&config); err != nil { - return err - } - b, err := json.Marshal(config) - if err != nil { - return err - } - *reply = b - return nil -} - -func (api *LocalAPI) RegisterCommand(args *model.Command, reply *APITeamReply) error { - return api.api.RegisterCommand(args) -} - -func (api *LocalAPI) UnregisterCommand(args *APIUnregisterCommandArgs, reply *APITeamReply) error { - return api.api.UnregisterCommand(args.TeamId, args.Trigger) -} - -type APIErrorReply struct { - Error *model.AppError -} - -type APITeamReply struct { - Team *model.Team - Error *model.AppError -} - -func (api *LocalAPI) CreateTeam(args *model.Team, reply *APITeamReply) error { - team, err := api.api.CreateTeam(args) - *reply = APITeamReply{ - Team: team, - Error: err, - } - return nil -} - -func (api *LocalAPI) DeleteTeam(args string, reply *APIErrorReply) error { - *reply = APIErrorReply{ - Error: api.api.DeleteTeam(args), - } - return nil -} - -func (api *LocalAPI) GetTeam(args string, reply *APITeamReply) error { - team, err := api.api.GetTeam(args) - *reply = APITeamReply{ - Team: team, - Error: err, - } - return nil -} - -func (api *LocalAPI) GetTeamByName(args string, reply *APITeamReply) error { - team, err := api.api.GetTeamByName(args) - *reply = APITeamReply{ - Team: team, - Error: err, - } - return nil -} - -func (api *LocalAPI) UpdateTeam(args *model.Team, reply *APITeamReply) error { - team, err := api.api.UpdateTeam(args) - *reply = APITeamReply{ - Team: team, - Error: err, - } - return nil -} - -type APIUserReply struct { - User *model.User - Error *model.AppError -} - -func (api *LocalAPI) CreateUser(args *model.User, reply *APIUserReply) error { - user, err := api.api.CreateUser(args) - *reply = APIUserReply{ - User: user, - Error: err, - } - return nil -} - -func (api *LocalAPI) DeleteUser(args string, reply *APIErrorReply) error { - *reply = APIErrorReply{ - Error: api.api.DeleteUser(args), - } - return nil -} - -func (api *LocalAPI) GetUser(args string, reply *APIUserReply) error { - user, err := api.api.GetUser(args) - *reply = APIUserReply{ - User: user, - Error: err, - } - return nil -} - -func (api *LocalAPI) GetUserByEmail(args string, reply *APIUserReply) error { - user, err := api.api.GetUserByEmail(args) - *reply = APIUserReply{ - User: user, - Error: err, - } - return nil -} - -func (api *LocalAPI) GetUserByUsername(args string, reply *APIUserReply) error { - user, err := api.api.GetUserByUsername(args) - *reply = APIUserReply{ - User: user, - Error: err, - } - return nil -} - -func (api *LocalAPI) UpdateUser(args *model.User, reply *APIUserReply) error { - user, err := api.api.UpdateUser(args) - *reply = APIUserReply{ - User: user, - Error: err, - } - return nil -} - -type APIGetChannelByNameArgs struct { - Name string - TeamId string -} - -type APIGetDirectChannelArgs struct { - UserId1 string - UserId2 string -} - -type APIGetGroupChannelArgs struct { - UserIds []string -} - -type APIAddChannelMemberArgs struct { - ChannelId string - UserId string -} - -type APIGetChannelMemberArgs struct { - ChannelId string - UserId string -} - -type APIUpdateChannelMemberRolesArgs struct { - ChannelId string - UserId string - NewRoles string -} - -type APIUpdateChannelMemberNotificationsArgs struct { - ChannelId string - UserId string - Notifications map[string]string -} - -type APIDeleteChannelMemberArgs struct { - ChannelId string - UserId string -} - -type APIChannelReply struct { - Channel *model.Channel - Error *model.AppError -} - -type APIChannelMemberReply struct { - ChannelMember *model.ChannelMember - Error *model.AppError -} - -func (api *LocalAPI) CreateChannel(args *model.Channel, reply *APIChannelReply) error { - channel, err := api.api.CreateChannel(args) - *reply = APIChannelReply{ - Channel: channel, - Error: err, - } - return nil -} - -func (api *LocalAPI) DeleteChannel(args string, reply *APIErrorReply) error { - *reply = APIErrorReply{ - Error: api.api.DeleteChannel(args), - } - return nil -} - -func (api *LocalAPI) GetChannel(args string, reply *APIChannelReply) error { - channel, err := api.api.GetChannel(args) - *reply = APIChannelReply{ - Channel: channel, - Error: err, - } - return nil -} - -func (api *LocalAPI) GetChannelByName(args *APIGetChannelByNameArgs, reply *APIChannelReply) error { - channel, err := api.api.GetChannelByName(args.Name, args.TeamId) - *reply = APIChannelReply{ - Channel: channel, - Error: err, - } - return nil -} - -func (api *LocalAPI) GetDirectChannel(args *APIGetDirectChannelArgs, reply *APIChannelReply) error { - channel, err := api.api.GetDirectChannel(args.UserId1, args.UserId2) - *reply = APIChannelReply{ - Channel: channel, - Error: err, - } - return nil -} - -func (api *LocalAPI) GetGroupChannel(args *APIGetGroupChannelArgs, reply *APIChannelReply) error { - channel, err := api.api.GetGroupChannel(args.UserIds) - *reply = APIChannelReply{ - Channel: channel, - Error: err, - } - return nil -} - -func (api *LocalAPI) UpdateChannel(args *model.Channel, reply *APIChannelReply) error { - channel, err := api.api.UpdateChannel(args) - *reply = APIChannelReply{ - Channel: channel, - Error: err, - } - return nil -} - -func (api *LocalAPI) AddChannelMember(args *APIAddChannelMemberArgs, reply *APIChannelMemberReply) error { - member, err := api.api.AddChannelMember(args.ChannelId, args.UserId) - *reply = APIChannelMemberReply{ - ChannelMember: member, - Error: err, - } - return nil -} - -func (api *LocalAPI) GetChannelMember(args *APIGetChannelMemberArgs, reply *APIChannelMemberReply) error { - member, err := api.api.GetChannelMember(args.ChannelId, args.UserId) - *reply = APIChannelMemberReply{ - ChannelMember: member, - Error: err, - } - return nil -} - -func (api *LocalAPI) UpdateChannelMemberRoles(args *APIUpdateChannelMemberRolesArgs, reply *APIChannelMemberReply) error { - member, err := api.api.UpdateChannelMemberRoles(args.ChannelId, args.UserId, args.NewRoles) - *reply = APIChannelMemberReply{ - ChannelMember: member, - Error: err, - } - return nil -} - -func (api *LocalAPI) UpdateChannelMemberNotifications(args *APIUpdateChannelMemberNotificationsArgs, reply *APIChannelMemberReply) error { - member, err := api.api.UpdateChannelMemberNotifications(args.ChannelId, args.UserId, args.Notifications) - *reply = APIChannelMemberReply{ - ChannelMember: member, - Error: err, - } - return nil -} - -func (api *LocalAPI) DeleteChannelMember(args *APIDeleteChannelMemberArgs, reply *APIErrorReply) error { - err := api.api.DeleteChannelMember(args.ChannelId, args.UserId) - *reply = APIErrorReply{ - Error: err, - } - return nil -} - -type APIPostReply struct { - Post *model.Post - Error *model.AppError -} - -func (api *LocalAPI) CreatePost(args *model.Post, reply *APIPostReply) error { - post, err := api.api.CreatePost(args) - *reply = APIPostReply{ - Post: post, - Error: err, - } - return nil -} - -func (api *LocalAPI) DeletePost(args string, reply *APIErrorReply) error { - *reply = APIErrorReply{ - Error: api.api.DeletePost(args), - } - return nil -} - -func (api *LocalAPI) GetPost(args string, reply *APIPostReply) error { - post, err := api.api.GetPost(args) - *reply = APIPostReply{ - Post: post, - Error: err, - } - return nil -} - -func (api *LocalAPI) UpdatePost(args *model.Post, reply *APIPostReply) error { - post, err := api.api.UpdatePost(args) - *reply = APIPostReply{ - Post: post, - Error: err, - } - return nil -} - -type APIKeyValueStoreReply struct { - Value []byte - Error *model.AppError -} - -type APIKeyValueStoreSetArgs struct { - Key string - Value []byte -} - -func (api *LocalAPI) KeyValueStoreSet(args *APIKeyValueStoreSetArgs, reply *APIErrorReply) error { - err := api.api.KeyValueStore().Set(args.Key, args.Value) - *reply = APIErrorReply{ - Error: err, - } - return nil -} - -func (api *LocalAPI) KeyValueStoreGet(args string, reply *APIKeyValueStoreReply) error { - v, err := api.api.KeyValueStore().Get(args) - *reply = APIKeyValueStoreReply{ - Value: v, - Error: err, - } - return nil -} - -func (api *LocalAPI) KeyValueStoreDelete(args string, reply *APIErrorReply) error { - err := api.api.KeyValueStore().Delete(args) - *reply = APIErrorReply{ - Error: err, - } - return nil -} - -func ServeAPI(api plugin.API, conn io.ReadWriteCloser, muxer *Muxer) { - server := rpc.NewServer() - server.Register(&LocalAPI{ - api: api, - muxer: muxer, - }) - server.ServeConn(conn) -} - -type RemoteAPI struct { - client *rpc.Client - muxer *Muxer - keyValueStore *RemoteKeyValueStore -} - -type RemoteKeyValueStore struct { - api *RemoteAPI -} - -var _ plugin.API = (*RemoteAPI)(nil) -var _ plugin.KeyValueStore = (*RemoteKeyValueStore)(nil) - -func (api *RemoteAPI) LoadPluginConfiguration(dest interface{}) error { - var config []byte - if err := api.client.Call("LocalAPI.LoadPluginConfiguration", struct{}{}, &config); err != nil { - return err - } - return json.Unmarshal(config, dest) -} - -func (api *RemoteAPI) RegisterCommand(command *model.Command) error { - return api.client.Call("LocalAPI.RegisterCommand", command, nil) -} - -type APIUnregisterCommandArgs struct { - TeamId string - Trigger string -} - -func (api *RemoteAPI) UnregisterCommand(teamId, trigger string) error { - return api.client.Call("LocalAPI.UnregisterCommand", &APIUnregisterCommandArgs{ - TeamId: teamId, - Trigger: trigger, - }, nil) -} - -func (api *RemoteAPI) CreateUser(user *model.User) (*model.User, *model.AppError) { - var reply APIUserReply - if err := api.client.Call("LocalAPI.CreateUser", user, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.CreateUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.User, reply.Error -} - -func (api *RemoteAPI) DeleteUser(userId string) *model.AppError { - var reply APIErrorReply - if err := api.client.Call("LocalAPI.DeleteUser", userId, &reply); err != nil { - return model.NewAppError("RemoteAPI.DeleteUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Error -} - -func (api *RemoteAPI) GetUser(userId string) (*model.User, *model.AppError) { - var reply APIUserReply - if err := api.client.Call("LocalAPI.GetUser", userId, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.User, reply.Error -} - -func (api *RemoteAPI) GetUserByEmail(email string) (*model.User, *model.AppError) { - var reply APIUserReply - if err := api.client.Call("LocalAPI.GetUserByEmail", email, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetUserByEmail", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.User, reply.Error -} - -func (api *RemoteAPI) GetUserByUsername(name string) (*model.User, *model.AppError) { - var reply APIUserReply - if err := api.client.Call("LocalAPI.GetUserByUsername", name, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetUserByUsername", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.User, reply.Error -} - -func (api *RemoteAPI) UpdateUser(user *model.User) (*model.User, *model.AppError) { - var reply APIUserReply - if err := api.client.Call("LocalAPI.UpdateUser", user, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.UpdateUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.User, reply.Error -} - -func (api *RemoteAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) { - var reply APITeamReply - if err := api.client.Call("LocalAPI.CreateTeam", team, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.CreateTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Team, reply.Error -} - -func (api *RemoteAPI) DeleteTeam(teamId string) *model.AppError { - var reply APIErrorReply - if err := api.client.Call("LocalAPI.DeleteTeam", teamId, &reply); err != nil { - return model.NewAppError("RemoteAPI.DeleteTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Error -} - -func (api *RemoteAPI) GetTeam(teamId string) (*model.Team, *model.AppError) { - var reply APITeamReply - if err := api.client.Call("LocalAPI.GetTeam", teamId, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Team, reply.Error -} - -func (api *RemoteAPI) GetTeamByName(name string) (*model.Team, *model.AppError) { - var reply APITeamReply - if err := api.client.Call("LocalAPI.GetTeamByName", name, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetTeamByName", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Team, reply.Error -} - -func (api *RemoteAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) { - var reply APITeamReply - if err := api.client.Call("LocalAPI.UpdateTeam", team, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.UpdateTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Team, reply.Error -} - -func (api *RemoteAPI) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { - var reply APIChannelReply - if err := api.client.Call("LocalAPI.CreateChannel", channel, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.CreateChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Channel, reply.Error -} - -func (api *RemoteAPI) DeleteChannel(channelId string) *model.AppError { - var reply APIErrorReply - if err := api.client.Call("LocalAPI.DeleteChannel", channelId, &reply); err != nil { - return model.NewAppError("RemoteAPI.DeleteChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Error -} - -func (api *RemoteAPI) GetChannel(channelId string) (*model.Channel, *model.AppError) { - var reply APIChannelReply - if err := api.client.Call("LocalAPI.GetChannel", channelId, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Channel, reply.Error -} - -func (api *RemoteAPI) GetChannelByName(name, teamId string) (*model.Channel, *model.AppError) { - var reply APIChannelReply - if err := api.client.Call("LocalAPI.GetChannelByName", &APIGetChannelByNameArgs{ - Name: name, - TeamId: teamId, - }, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetChannelByName", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Channel, reply.Error -} - -func (api *RemoteAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) { - var reply APIChannelReply - if err := api.client.Call("LocalAPI.GetDirectChannel", &APIGetDirectChannelArgs{ - UserId1: userId1, - UserId2: userId2, - }, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetDirectChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Channel, reply.Error -} - -func (api *RemoteAPI) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) { - var reply APIChannelReply - if err := api.client.Call("LocalAPI.GetGroupChannel", &APIGetGroupChannelArgs{ - UserIds: userIds, - }, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetGroupChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Channel, reply.Error -} - -func (api *RemoteAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) { - var reply APIChannelReply - if err := api.client.Call("LocalAPI.UpdateChannel", channel, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.UpdateChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Channel, reply.Error -} - -func (api *RemoteAPI) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) { - var reply APIChannelMemberReply - if err := api.client.Call("LocalAPI.AddChannelMember", &APIAddChannelMemberArgs{ - ChannelId: channelId, - UserId: userId, - }, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.AddChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.ChannelMember, reply.Error -} - -func (api *RemoteAPI) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) { - var reply APIChannelMemberReply - if err := api.client.Call("LocalAPI.GetChannelMember", &APIGetChannelMemberArgs{ - ChannelId: channelId, - UserId: userId, - }, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.ChannelMember, reply.Error -} - -func (api *RemoteAPI) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) { - var reply APIChannelMemberReply - if err := api.client.Call("LocalAPI.UpdateChannelMemberRoles", &APIUpdateChannelMemberRolesArgs{ - ChannelId: channelId, - UserId: userId, - NewRoles: newRoles, - }, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.UpdateChannelMemberRoles", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.ChannelMember, reply.Error -} - -func (api *RemoteAPI) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) { - var reply APIChannelMemberReply - if err := api.client.Call("LocalAPI.UpdateChannelMemberNotifications", &APIUpdateChannelMemberNotificationsArgs{ - ChannelId: channelId, - UserId: userId, - Notifications: notifications, - }, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.UpdateChannelMemberNotifications", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.ChannelMember, reply.Error -} - -func (api *RemoteAPI) DeleteChannelMember(channelId, userId string) *model.AppError { - var reply APIErrorReply - if err := api.client.Call("LocalAPI.DeleteChannelMember", &APIDeleteChannelMemberArgs{ - ChannelId: channelId, - UserId: userId, - }, &reply); err != nil { - return model.NewAppError("RemoteAPI.DeleteChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Error -} - -func (api *RemoteAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) { - var reply APIPostReply - if err := api.client.Call("LocalAPI.CreatePost", post, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.CreatePost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Post, reply.Error -} - -func (api *RemoteAPI) DeletePost(postId string) *model.AppError { - var reply APIErrorReply - if err := api.client.Call("LocalAPI.DeletePost", postId, &reply); err != nil { - return model.NewAppError("RemoteAPI.DeletePost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Error -} - -func (api *RemoteAPI) GetPost(postId string) (*model.Post, *model.AppError) { - var reply APIPostReply - if err := api.client.Call("LocalAPI.GetPost", postId, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.GetPost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Post, reply.Error -} - -func (api *RemoteAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError) { - var reply APIPostReply - if err := api.client.Call("LocalAPI.UpdatePost", post, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.UpdatePost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Post, reply.Error -} - -func (api *RemoteAPI) KeyValueStore() plugin.KeyValueStore { - return api.keyValueStore -} - -func (s *RemoteKeyValueStore) Set(key string, value []byte) *model.AppError { - var reply APIErrorReply - if err := s.api.client.Call("LocalAPI.KeyValueStoreSet", &APIKeyValueStoreSetArgs{Key: key, Value: value}, &reply); err != nil { - return model.NewAppError("RemoteAPI.KeyValueStoreSet", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Error -} - -func (s *RemoteKeyValueStore) Get(key string) ([]byte, *model.AppError) { - var reply APIKeyValueStoreReply - if err := s.api.client.Call("LocalAPI.KeyValueStoreGet", key, &reply); err != nil { - return nil, model.NewAppError("RemoteAPI.KeyValueStoreGet", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Value, reply.Error -} - -func (s *RemoteKeyValueStore) Delete(key string) *model.AppError { - var reply APIErrorReply - if err := s.api.client.Call("LocalAPI.KeyValueStoreDelete", key, &reply); err != nil { - return model.NewAppError("RemoteAPI.KeyValueStoreDelete", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Error -} - -func (h *RemoteAPI) Close() error { - return h.client.Close() -} - -func ConnectAPI(conn io.ReadWriteCloser, muxer *Muxer) *RemoteAPI { - remoteKeyValueStore := &RemoteKeyValueStore{} - remoteApi := &RemoteAPI{ - client: rpc.NewClient(conn), - muxer: muxer, - keyValueStore: remoteKeyValueStore, - } - - remoteKeyValueStore.api = remoteApi - - return remoteApi -} - -func init() { - gob.Register([]*model.SlackAttachment{}) - gob.Register([]interface{}{}) - gob.Register(map[string]interface{}{}) -} diff --git a/plugin/rpcplugin/api_test.go b/plugin/rpcplugin/api_test.go deleted file mode 100644 index 04d8e5d86..000000000 --- a/plugin/rpcplugin/api_test.go +++ /dev/null @@ -1,300 +0,0 @@ -package rpcplugin - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/plugin/plugintest" -) - -func testAPIRPC(api plugin.API, f func(plugin.API)) { - r1, w1 := io.Pipe() - r2, w2 := io.Pipe() - - c1 := NewMuxer(NewReadWriteCloser(r1, w2), false) - defer c1.Close() - - c2 := NewMuxer(NewReadWriteCloser(r2, w1), true) - defer c2.Close() - - id, server := c1.Serve() - go ServeAPI(api, server, c1) - - remote := ConnectAPI(c2.Connect(id), c2) - defer remote.Close() - - f(remote) -} - -func TestAPI(t *testing.T) { - keyValueStore := &plugintest.KeyValueStore{} - api := plugintest.API{Store: keyValueStore} - defer api.AssertExpectations(t) - - type Config struct { - Foo string - Bar struct { - Baz string - } - } - - api.On("LoadPluginConfiguration", mock.MatchedBy(func(x interface{}) bool { return true })).Run(func(args mock.Arguments) { - dest := args.Get(0).(interface{}) - json.Unmarshal([]byte(`{"Foo": "foo", "Bar": {"Baz": "baz"}}`), dest) - }).Return(nil) - - testChannel := &model.Channel{ - Id: "thechannelid", - } - - testChannelMember := &model.ChannelMember{ - ChannelId: "thechannelid", - UserId: "theuserid", - } - - testTeam := &model.Team{ - Id: "theteamid", - } - teamNotFoundError := model.NewAppError("SqlTeamStore.GetByName", "store.sql_team.get_by_name.app_error", nil, "name=notateam", http.StatusNotFound) - - testUser := &model.User{ - Id: "theuserid", - } - - testPost := &model.Post{ - Message: "hello", - } - - testAPIRPC(&api, func(remote plugin.API) { - var config Config - assert.NoError(t, remote.LoadPluginConfiguration(&config)) - assert.Equal(t, "foo", config.Foo) - assert.Equal(t, "baz", config.Bar.Baz) - - api.On("RegisterCommand", mock.AnythingOfType("*model.Command")).Return(fmt.Errorf("foo")).Once() - assert.Error(t, remote.RegisterCommand(&model.Command{})) - api.On("RegisterCommand", mock.AnythingOfType("*model.Command")).Return(nil).Once() - assert.NoError(t, remote.RegisterCommand(&model.Command{})) - - api.On("UnregisterCommand", "team", "trigger").Return(fmt.Errorf("foo")).Once() - assert.Error(t, remote.UnregisterCommand("team", "trigger")) - api.On("UnregisterCommand", "team", "trigger").Return(nil).Once() - assert.NoError(t, remote.UnregisterCommand("team", "trigger")) - - api.On("CreateChannel", mock.AnythingOfType("*model.Channel")).Return(func(c *model.Channel) *model.Channel { - c.Id = "thechannelid" - return c - }, nil).Once() - channel, err := remote.CreateChannel(testChannel) - assert.Equal(t, "thechannelid", channel.Id) - assert.Nil(t, err) - - api.On("DeleteChannel", "thechannelid").Return(nil).Once() - assert.Nil(t, remote.DeleteChannel("thechannelid")) - - api.On("GetChannel", "thechannelid").Return(testChannel, nil).Once() - channel, err = remote.GetChannel("thechannelid") - assert.Equal(t, testChannel, channel) - assert.Nil(t, err) - - api.On("GetChannelByName", "foo", "theteamid").Return(testChannel, nil).Once() - channel, err = remote.GetChannelByName("foo", "theteamid") - assert.Equal(t, testChannel, channel) - assert.Nil(t, err) - - api.On("GetDirectChannel", "user1", "user2").Return(testChannel, nil).Once() - channel, err = remote.GetDirectChannel("user1", "user2") - assert.Equal(t, testChannel, channel) - assert.Nil(t, err) - - api.On("GetGroupChannel", []string{"user1", "user2", "user3"}).Return(testChannel, nil).Once() - channel, err = remote.GetGroupChannel([]string{"user1", "user2", "user3"}) - assert.Equal(t, testChannel, channel) - assert.Nil(t, err) - - api.On("UpdateChannel", mock.AnythingOfType("*model.Channel")).Return(func(c *model.Channel) *model.Channel { - return c - }, nil).Once() - channel, err = remote.UpdateChannel(testChannel) - assert.Equal(t, testChannel, channel) - assert.Nil(t, err) - - api.On("AddChannelMember", testChannel.Id, "theuserid").Return(testChannelMember, nil).Once() - member, err := remote.AddChannelMember(testChannel.Id, "theuserid") - assert.Equal(t, testChannelMember, member) - assert.Nil(t, err) - - api.On("GetChannelMember", "thechannelid", "theuserid").Return(testChannelMember, nil).Once() - member, err = remote.GetChannelMember("thechannelid", "theuserid") - assert.Equal(t, testChannelMember, member) - assert.Nil(t, err) - - api.On("UpdateChannelMemberRoles", testChannel.Id, "theuserid", model.CHANNEL_ADMIN_ROLE_ID).Return(testChannelMember, nil).Once() - member, err = remote.UpdateChannelMemberRoles(testChannel.Id, "theuserid", model.CHANNEL_ADMIN_ROLE_ID) - assert.Equal(t, testChannelMember, member) - assert.Nil(t, err) - - notifications := map[string]string{} - notifications[model.MARK_UNREAD_NOTIFY_PROP] = model.CHANNEL_MARK_UNREAD_MENTION - api.On("UpdateChannelMemberNotifications", testChannel.Id, "theuserid", notifications).Return(testChannelMember, nil).Once() - member, err = remote.UpdateChannelMemberNotifications(testChannel.Id, "theuserid", notifications) - assert.Equal(t, testChannelMember, member) - assert.Nil(t, err) - - api.On("DeleteChannelMember", "thechannelid", "theuserid").Return(nil).Once() - err = remote.DeleteChannelMember("thechannelid", "theuserid") - assert.Nil(t, err) - - api.On("CreateUser", mock.AnythingOfType("*model.User")).Return(func(u *model.User) *model.User { - u.Id = "theuserid" - return u - }, nil).Once() - user, err := remote.CreateUser(testUser) - assert.Equal(t, "theuserid", user.Id) - assert.Nil(t, err) - - api.On("DeleteUser", "theuserid").Return(nil).Once() - assert.Nil(t, remote.DeleteUser("theuserid")) - - api.On("GetUser", "theuserid").Return(testUser, nil).Once() - user, err = remote.GetUser("theuserid") - assert.Equal(t, testUser, user) - assert.Nil(t, err) - - api.On("GetUserByEmail", "foo@foo").Return(testUser, nil).Once() - user, err = remote.GetUserByEmail("foo@foo") - assert.Equal(t, testUser, user) - assert.Nil(t, err) - - api.On("GetUserByUsername", "foo").Return(testUser, nil).Once() - user, err = remote.GetUserByUsername("foo") - assert.Equal(t, testUser, user) - assert.Nil(t, err) - - api.On("UpdateUser", mock.AnythingOfType("*model.User")).Return(func(u *model.User) *model.User { - return u - }, nil).Once() - user, err = remote.UpdateUser(testUser) - assert.Equal(t, testUser, user) - assert.Nil(t, err) - - api.On("CreateTeam", mock.AnythingOfType("*model.Team")).Return(func(t *model.Team) *model.Team { - t.Id = "theteamid" - return t - }, nil).Once() - team, err := remote.CreateTeam(testTeam) - assert.Equal(t, "theteamid", team.Id) - assert.Nil(t, err) - - api.On("DeleteTeam", "theteamid").Return(nil).Once() - assert.Nil(t, remote.DeleteTeam("theteamid")) - - api.On("GetTeam", "theteamid").Return(testTeam, nil).Once() - team, err = remote.GetTeam("theteamid") - assert.Equal(t, testTeam, team) - assert.Nil(t, err) - - api.On("GetTeamByName", "foo").Return(testTeam, nil).Once() - team, err = remote.GetTeamByName("foo") - assert.Equal(t, testTeam, team) - assert.Nil(t, err) - - api.On("GetTeamByName", "notateam").Return(nil, teamNotFoundError).Once() - team, err = remote.GetTeamByName("notateam") - assert.Nil(t, team) - assert.Equal(t, teamNotFoundError, err) - - api.On("UpdateTeam", mock.AnythingOfType("*model.Team")).Return(func(t *model.Team) *model.Team { - return t - }, nil).Once() - team, err = remote.UpdateTeam(testTeam) - assert.Equal(t, testTeam, team) - assert.Nil(t, err) - - api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(func(p *model.Post) *model.Post { - p.Id = "thepostid" - return p - }, nil).Once() - post, err := remote.CreatePost(testPost) - require.Nil(t, err) - assert.NotEmpty(t, post.Id) - assert.Equal(t, testPost.Message, post.Message) - - api.On("DeletePost", "thepostid").Return(nil).Once() - assert.Nil(t, remote.DeletePost("thepostid")) - - api.On("GetPost", "thepostid").Return(testPost, nil).Once() - post, err = remote.GetPost("thepostid") - assert.Equal(t, testPost, post) - assert.Nil(t, err) - - api.On("UpdatePost", mock.AnythingOfType("*model.Post")).Return(func(p *model.Post) *model.Post { - return p - }, nil).Once() - post, err = remote.UpdatePost(testPost) - assert.Equal(t, testPost, post) - assert.Nil(t, err) - - api.KeyValueStore().(*plugintest.KeyValueStore).On("Set", "thekey", []byte("thevalue")).Return(nil).Once() - err = remote.KeyValueStore().Set("thekey", []byte("thevalue")) - assert.Nil(t, err) - - api.KeyValueStore().(*plugintest.KeyValueStore).On("Get", "thekey").Return(func(key string) []byte { - return []byte("thevalue") - }, nil).Once() - ret, err := remote.KeyValueStore().Get("thekey") - assert.Nil(t, err) - assert.Equal(t, []byte("thevalue"), ret) - - api.KeyValueStore().(*plugintest.KeyValueStore).On("Delete", "thekey").Return(nil).Once() - err = remote.KeyValueStore().Delete("thekey") - assert.Nil(t, err) - }) -} - -func TestAPI_GobRegistration(t *testing.T) { - keyValueStore := &plugintest.KeyValueStore{} - api := plugintest.API{Store: keyValueStore} - defer api.AssertExpectations(t) - - testAPIRPC(&api, func(remote plugin.API) { - api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(func(p *model.Post) *model.Post { - p.Id = "thepostid" - return p - }, nil).Once() - _, err := remote.CreatePost(&model.Post{ - Message: "hello", - Props: map[string]interface{}{ - "attachments": []*model.SlackAttachment{ - &model.SlackAttachment{ - Actions: []*model.PostAction{ - &model.PostAction{ - Integration: &model.PostActionIntegration{ - Context: map[string]interface{}{ - "foo": "bar", - "foos": []interface{}{"bar", "baz", 1, 2}, - "foo_map": map[string]interface{}{ - "1": "bar", - "2": 2, - }, - }, - }, - }, - }, - Timestamp: 1, - }, - }, - }, - }) - require.Nil(t, err) - }) -} diff --git a/plugin/rpcplugin/hooks.go b/plugin/rpcplugin/hooks.go deleted file mode 100644 index 6af98873a..000000000 --- a/plugin/rpcplugin/hooks.go +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "bytes" - "io" - "io/ioutil" - "net/http" - "net/rpc" - "reflect" - - "github.com/mattermost/mattermost-server/mlog" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" -) - -type LocalHooks struct { - hooks interface{} - muxer *Muxer - remoteAPI *RemoteAPI -} - -// Implemented replies with the names of the hooks that are implemented. -func (h *LocalHooks) Implemented(args struct{}, reply *[]string) error { - ifaceType := reflect.TypeOf((*plugin.Hooks)(nil)).Elem() - implType := reflect.TypeOf(h.hooks) - selfType := reflect.TypeOf(h) - var methods []string - for i := 0; i < ifaceType.NumMethod(); i++ { - method := ifaceType.Method(i) - if m, ok := implType.MethodByName(method.Name); !ok { - continue - } else if m.Type.NumIn() != method.Type.NumIn()+1 { - continue - } else if m.Type.NumOut() != method.Type.NumOut() { - continue - } else { - match := true - for j := 0; j < method.Type.NumIn(); j++ { - if m.Type.In(j+1) != method.Type.In(j) { - match = false - break - } - } - for j := 0; j < method.Type.NumOut(); j++ { - if m.Type.Out(j) != method.Type.Out(j) { - match = false - break - } - } - if !match { - continue - } - } - if _, ok := selfType.MethodByName(method.Name); !ok { - continue - } - methods = append(methods, method.Name) - } - *reply = methods - return nil -} - -func (h *LocalHooks) OnActivate(args int64, reply *struct{}) error { - if h.remoteAPI != nil { - h.remoteAPI.Close() - h.remoteAPI = nil - } - if hook, ok := h.hooks.(interface { - OnActivate(plugin.API) error - }); ok { - stream := h.muxer.Connect(args) - h.remoteAPI = ConnectAPI(stream, h.muxer) - return hook.OnActivate(h.remoteAPI) - } - return nil -} - -func (h *LocalHooks) OnDeactivate(args, reply *struct{}) (err error) { - if hook, ok := h.hooks.(interface { - OnDeactivate() error - }); ok { - err = hook.OnDeactivate() - } - if h.remoteAPI != nil { - h.remoteAPI.Close() - h.remoteAPI = nil - } - return -} - -func (h *LocalHooks) OnConfigurationChange(args, reply *struct{}) error { - if hook, ok := h.hooks.(interface { - OnConfigurationChange() error - }); ok { - return hook.OnConfigurationChange() - } - return nil -} - -type ServeHTTPArgs struct { - ResponseWriterStream int64 - Request *http.Request - RequestBodyStream int64 -} - -func (h *LocalHooks) ServeHTTP(args ServeHTTPArgs, reply *struct{}) error { - w := ConnectHTTPResponseWriter(h.muxer.Connect(args.ResponseWriterStream)) - defer w.Close() - - r := args.Request - if args.RequestBodyStream != 0 { - r.Body = ConnectIOReader(h.muxer.Connect(args.RequestBodyStream)) - } else { - r.Body = ioutil.NopCloser(&bytes.Buffer{}) - } - defer r.Body.Close() - - if hook, ok := h.hooks.(http.Handler); ok { - hook.ServeHTTP(w, r) - } else { - http.NotFound(w, r) - } - - return nil -} - -type HooksExecuteCommandReply struct { - Response *model.CommandResponse - Error *model.AppError -} - -func (h *LocalHooks) ExecuteCommand(args *model.CommandArgs, reply *HooksExecuteCommandReply) error { - if hook, ok := h.hooks.(interface { - ExecuteCommand(*model.CommandArgs) (*model.CommandResponse, *model.AppError) - }); ok { - reply.Response, reply.Error = hook.ExecuteCommand(args) - } - return nil -} - -type MessageWillBeReply struct { - Post *model.Post - RejectionReason string -} - -type MessageUpdatedArgs struct { - NewPost *model.Post - OldPost *model.Post -} - -func (h *LocalHooks) MessageWillBePosted(args *model.Post, reply *MessageWillBeReply) error { - if hook, ok := h.hooks.(interface { - MessageWillBePosted(*model.Post) (*model.Post, string) - }); ok { - reply.Post, reply.RejectionReason = hook.MessageWillBePosted(args) - } - return nil -} - -func (h *LocalHooks) MessageWillBeUpdated(args *MessageUpdatedArgs, reply *MessageWillBeReply) error { - if hook, ok := h.hooks.(interface { - MessageWillBeUpdated(*model.Post, *model.Post) (*model.Post, string) - }); ok { - reply.Post, reply.RejectionReason = hook.MessageWillBeUpdated(args.NewPost, args.OldPost) - } - return nil -} - -func (h *LocalHooks) MessageHasBeenPosted(args *model.Post, reply *struct{}) error { - if hook, ok := h.hooks.(interface { - MessageHasBeenPosted(*model.Post) - }); ok { - hook.MessageHasBeenPosted(args) - } - return nil -} - -func (h *LocalHooks) MessageHasBeenUpdated(args *MessageUpdatedArgs, reply *struct{}) error { - if hook, ok := h.hooks.(interface { - MessageHasBeenUpdated(*model.Post, *model.Post) - }); ok { - hook.MessageHasBeenUpdated(args.NewPost, args.OldPost) - } - return nil -} - -func ServeHooks(hooks interface{}, conn io.ReadWriteCloser, muxer *Muxer) { - server := rpc.NewServer() - server.Register(&LocalHooks{ - hooks: hooks, - muxer: muxer, - }) - server.ServeConn(conn) -} - -// These assignments are part of the wire protocol. You can add more, but should not change existing -// assignments. -const ( - remoteOnActivate = 0 - remoteOnDeactivate = 1 - remoteServeHTTP = 2 - remoteOnConfigurationChange = 3 - remoteExecuteCommand = 4 - remoteMessageWillBePosted = 5 - remoteMessageWillBeUpdated = 6 - remoteMessageHasBeenPosted = 7 - remoteMessageHasBeenUpdated = 8 - maxRemoteHookCount = iota -) - -type RemoteHooks struct { - client *rpc.Client - muxer *Muxer - apiCloser io.Closer - implemented [maxRemoteHookCount]bool - pluginId string -} - -var _ plugin.Hooks = (*RemoteHooks)(nil) - -func (h *RemoteHooks) Implemented() (impl []string, err error) { - err = h.client.Call("LocalHooks.Implemented", struct{}{}, &impl) - return -} - -func (h *RemoteHooks) OnActivate(api plugin.API) error { - if h.apiCloser != nil { - h.apiCloser.Close() - h.apiCloser = nil - } - if !h.implemented[remoteOnActivate] { - return nil - } - id, stream := h.muxer.Serve() - h.apiCloser = stream - go ServeAPI(api, stream, h.muxer) - return h.client.Call("LocalHooks.OnActivate", id, nil) -} - -func (h *RemoteHooks) OnDeactivate() error { - if !h.implemented[remoteOnDeactivate] { - return nil - } - return h.client.Call("LocalHooks.OnDeactivate", struct{}{}, nil) -} - -func (h *RemoteHooks) OnConfigurationChange() error { - if !h.implemented[remoteOnConfigurationChange] { - return nil - } - return h.client.Call("LocalHooks.OnConfigurationChange", struct{}{}, nil) -} - -func (h *RemoteHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if !h.implemented[remoteServeHTTP] { - http.NotFound(w, r) - return - } - - responseWriterStream, stream := h.muxer.Serve() - defer stream.Close() - go ServeHTTPResponseWriter(w, stream) - - requestBodyStream := int64(0) - if r.Body != nil { - rid, rstream := h.muxer.Serve() - defer rstream.Close() - go ServeIOReader(r.Body, rstream) - requestBodyStream = rid - } - - forwardedRequest := &http.Request{ - Method: r.Method, - URL: r.URL, - Proto: r.Proto, - ProtoMajor: r.ProtoMajor, - ProtoMinor: r.ProtoMinor, - Header: r.Header, - Host: r.Host, - RemoteAddr: r.RemoteAddr, - RequestURI: r.RequestURI, - } - - if err := h.client.Call("LocalHooks.ServeHTTP", ServeHTTPArgs{ - ResponseWriterStream: responseWriterStream, - Request: forwardedRequest, - RequestBodyStream: requestBodyStream, - }, nil); err != nil { - mlog.Error("Plugin failed to ServeHTTP", mlog.String("plugin_id", h.pluginId), mlog.Err(err)) - http.Error(w, "500 internal server error", http.StatusInternalServerError) - } -} - -func (h *RemoteHooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - if !h.implemented[remoteExecuteCommand] { - return nil, model.NewAppError("RemoteHooks.ExecuteCommand", "plugin.rpcplugin.invocation.error", nil, "err=ExecuteCommand hook not implemented", http.StatusInternalServerError) - } - var reply HooksExecuteCommandReply - if err := h.client.Call("LocalHooks.ExecuteCommand", args, &reply); err != nil { - return nil, model.NewAppError("RemoteHooks.ExecuteCommand", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - return reply.Response, reply.Error -} - -func (h *RemoteHooks) MessageWillBePosted(args *model.Post) (*model.Post, string) { - if !h.implemented[remoteMessageWillBePosted] { - return args, "" - } - var reply MessageWillBeReply - if err := h.client.Call("LocalHooks.MessageWillBePosted", args, &reply); err != nil { - return nil, "" - } - return reply.Post, reply.RejectionReason -} - -func (h *RemoteHooks) MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string) { - if !h.implemented[remoteMessageWillBeUpdated] { - return newPost, "" - } - var reply MessageWillBeReply - args := &MessageUpdatedArgs{ - NewPost: newPost, - OldPost: oldPost, - } - if err := h.client.Call("LocalHooks.MessageWillBeUpdated", args, &reply); err != nil { - return nil, "" - } - return reply.Post, reply.RejectionReason -} - -func (h *RemoteHooks) MessageHasBeenPosted(args *model.Post) { - if !h.implemented[remoteMessageHasBeenPosted] { - return - } - if err := h.client.Call("LocalHooks.MessageHasBeenPosted", args, nil); err != nil { - return - } -} - -func (h *RemoteHooks) MessageHasBeenUpdated(newPost, oldPost *model.Post) { - if !h.implemented[remoteMessageHasBeenUpdated] { - return - } - args := &MessageUpdatedArgs{ - NewPost: newPost, - OldPost: oldPost, - } - if err := h.client.Call("LocalHooks.MessageHasBeenUpdated", args, nil); err != nil { - return - } -} - -func (h *RemoteHooks) Close() error { - if h.apiCloser != nil { - h.apiCloser.Close() - h.apiCloser = nil - } - return h.client.Close() -} - -func ConnectHooks(conn io.ReadWriteCloser, muxer *Muxer, pluginId string) (*RemoteHooks, error) { - remote := &RemoteHooks{ - client: rpc.NewClient(conn), - muxer: muxer, - pluginId: pluginId, - } - implemented, err := remote.Implemented() - if err != nil { - remote.Close() - return nil, err - } - for _, method := range implemented { - switch method { - case "OnActivate": - remote.implemented[remoteOnActivate] = true - case "OnDeactivate": - remote.implemented[remoteOnDeactivate] = true - case "OnConfigurationChange": - remote.implemented[remoteOnConfigurationChange] = true - case "ServeHTTP": - remote.implemented[remoteServeHTTP] = true - case "ExecuteCommand": - remote.implemented[remoteExecuteCommand] = true - case "MessageWillBePosted": - remote.implemented[remoteMessageWillBePosted] = true - case "MessageWillBeUpdated": - remote.implemented[remoteMessageWillBeUpdated] = true - case "MessageHasBeenPosted": - remote.implemented[remoteMessageHasBeenPosted] = true - case "MessageHasBeenUpdated": - remote.implemented[remoteMessageHasBeenUpdated] = true - } - } - return remote, nil -} diff --git a/plugin/rpcplugin/hooks_test.go b/plugin/rpcplugin/hooks_test.go deleted file mode 100644 index a7bac982e..000000000 --- a/plugin/rpcplugin/hooks_test.go +++ /dev/null @@ -1,237 +0,0 @@ -package rpcplugin - -import ( - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "strings" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/plugin/plugintest" -) - -func testHooksRPC(hooks interface{}, f func(*RemoteHooks)) error { - r1, w1 := io.Pipe() - r2, w2 := io.Pipe() - - c1 := NewMuxer(NewReadWriteCloser(r1, w2), false) - defer c1.Close() - - c2 := NewMuxer(NewReadWriteCloser(r2, w1), true) - defer c2.Close() - - id, server := c1.Serve() - go ServeHooks(hooks, server, c1) - - remote, err := ConnectHooks(c2.Connect(id), c2, "plugin_id") - if err != nil { - return err - } - defer remote.Close() - - f(remote) - return nil -} - -func TestHooks(t *testing.T) { - var api plugintest.API - var hooks plugintest.Hooks - defer hooks.AssertExpectations(t) - - assert.NoError(t, testHooksRPC(&hooks, func(remote *RemoteHooks) { - hooks.On("OnActivate", mock.AnythingOfType("*rpcplugin.RemoteAPI")).Return(nil) - assert.NoError(t, remote.OnActivate(&api)) - - hooks.On("OnDeactivate").Return(nil) - assert.NoError(t, remote.OnDeactivate()) - - hooks.On("OnConfigurationChange").Return(nil) - assert.NoError(t, remote.OnConfigurationChange()) - - hooks.On("ServeHTTP", mock.AnythingOfType("*rpcplugin.RemoteHTTPResponseWriter"), mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) { - w := args.Get(0).(http.ResponseWriter) - r := args.Get(1).(*http.Request) - assert.Equal(t, "/foo", r.URL.Path) - assert.Equal(t, "POST", r.Method) - body, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - assert.Equal(t, "asdf", string(body)) - assert.Equal(t, "header", r.Header.Get("Test-Header")) - w.Write([]byte("bar")) - }) - - w := httptest.NewRecorder() - r, err := http.NewRequest("POST", "/foo", strings.NewReader("asdf")) - r.Header.Set("Test-Header", "header") - assert.NoError(t, err) - remote.ServeHTTP(w, r) - - resp := w.Result() - defer resp.Body.Close() - assert.Equal(t, http.StatusOK, resp.StatusCode) - body, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - assert.Equal(t, "bar", string(body)) - - hooks.On("ExecuteCommand", &model.CommandArgs{ - Command: "/foo", - }).Return(&model.CommandResponse{ - Text: "bar", - }, nil) - commandResponse, appErr := hooks.ExecuteCommand(&model.CommandArgs{ - Command: "/foo", - }) - assert.Equal(t, "bar", commandResponse.Text) - assert.Nil(t, appErr) - - hooks.On("MessageWillBePosted", mock.AnythingOfType("*model.Post")).Return(func(post *model.Post) *model.Post { - post.Message += "_testing" - return post - }, "changemessage") - post, changemessage := remote.MessageWillBePosted(&model.Post{Id: "1", Message: "base"}) - assert.Equal(t, "changemessage", changemessage) - assert.Equal(t, "base_testing", post.Message) - assert.Equal(t, "1", post.Id) - - hooks.On("MessageWillBeUpdated", mock.AnythingOfType("*model.Post"), mock.AnythingOfType("*model.Post")).Return(func(newPost, oldPost *model.Post) *model.Post { - newPost.Message += "_testing" - return newPost - }, "changemessage2") - post2, changemessage2 := remote.MessageWillBeUpdated(&model.Post{Id: "2", Message: "base2"}, &model.Post{Id: "OLD", Message: "OLDMESSAGE"}) - assert.Equal(t, "changemessage2", changemessage2) - assert.Equal(t, "base2_testing", post2.Message) - assert.Equal(t, "2", post2.Id) - - hooks.On("MessageHasBeenPosted", mock.AnythingOfType("*model.Post")).Return(nil) - remote.MessageHasBeenPosted(&model.Post{}) - - hooks.On("MessageHasBeenUpdated", mock.AnythingOfType("*model.Post"), mock.AnythingOfType("*model.Post")).Return(nil) - remote.MessageHasBeenUpdated(&model.Post{}, &model.Post{}) - })) -} - -func TestHooks_Concurrency(t *testing.T) { - var hooks plugintest.Hooks - defer hooks.AssertExpectations(t) - - assert.NoError(t, testHooksRPC(&hooks, func(remote *RemoteHooks) { - ch := make(chan bool) - - hooks.On("ServeHTTP", mock.AnythingOfType("*rpcplugin.RemoteHTTPResponseWriter"), mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) { - r := args.Get(1).(*http.Request) - if r.URL.Path == "/1" { - <-ch - } else { - ch <- true - } - }) - - rec := httptest.NewRecorder() - - wg := sync.WaitGroup{} - wg.Add(2) - - go func() { - req, err := http.NewRequest("GET", "/1", nil) - require.NoError(t, err) - remote.ServeHTTP(rec, req) - wg.Done() - }() - - go func() { - req, err := http.NewRequest("GET", "/2", nil) - require.NoError(t, err) - remote.ServeHTTP(rec, req) - wg.Done() - }() - - wg.Wait() - })) -} - -type testHooks struct { - mock.Mock -} - -func (h *testHooks) OnActivate(api plugin.API) error { - return h.Called(api).Error(0) -} - -func TestHooks_PartiallyImplemented(t *testing.T) { - var api plugintest.API - var hooks testHooks - defer hooks.AssertExpectations(t) - - assert.NoError(t, testHooksRPC(&hooks, func(remote *RemoteHooks) { - implemented, err := remote.Implemented() - assert.NoError(t, err) - assert.Equal(t, []string{"OnActivate"}, implemented) - - hooks.On("OnActivate", mock.AnythingOfType("*rpcplugin.RemoteAPI")).Return(nil) - assert.NoError(t, remote.OnActivate(&api)) - - assert.NoError(t, remote.OnDeactivate()) - })) -} - -type benchmarkHooks struct{} - -func (*benchmarkHooks) OnDeactivate() error { return nil } - -func (*benchmarkHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ioutil.ReadAll(r.Body) - w.Header().Set("Foo-Header", "foo") - http.Error(w, "foo", http.StatusBadRequest) -} - -func BenchmarkHooks_OnDeactivate(b *testing.B) { - var hooks benchmarkHooks - - if err := testHooksRPC(&hooks, func(remote *RemoteHooks) { - b.ResetTimer() - for n := 0; n < b.N; n++ { - remote.OnDeactivate() - } - b.StopTimer() - }); err != nil { - b.Fatal(err.Error()) - } -} - -func BenchmarkHooks_ServeHTTP(b *testing.B) { - var hooks benchmarkHooks - - if err := testHooksRPC(&hooks, func(remote *RemoteHooks) { - b.ResetTimer() - for n := 0; n < b.N; n++ { - w := httptest.NewRecorder() - r, _ := http.NewRequest("POST", "/foo", strings.NewReader("12345678901234567890")) - remote.ServeHTTP(w, r) - } - b.StopTimer() - }); err != nil { - b.Fatal(err.Error()) - } -} - -func BenchmarkHooks_Unimplemented(b *testing.B) { - var hooks testHooks - - if err := testHooksRPC(&hooks, func(remote *RemoteHooks) { - b.ResetTimer() - for n := 0; n < b.N; n++ { - remote.OnDeactivate() - } - b.StopTimer() - }); err != nil { - b.Fatal(err.Error()) - } -} diff --git a/plugin/rpcplugin/http.go b/plugin/rpcplugin/http.go deleted file mode 100644 index 72b1aa445..000000000 --- a/plugin/rpcplugin/http.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "io" - "net/http" - "net/rpc" -) - -type LocalHTTPResponseWriter struct { - w http.ResponseWriter -} - -func (w *LocalHTTPResponseWriter) Header(args struct{}, reply *http.Header) error { - *reply = w.w.Header() - return nil -} - -func (w *LocalHTTPResponseWriter) Write(args []byte, reply *struct{}) error { - _, err := w.w.Write(args) - return err -} - -func (w *LocalHTTPResponseWriter) WriteHeader(args int, reply *struct{}) error { - w.w.WriteHeader(args) - return nil -} - -func (w *LocalHTTPResponseWriter) SyncHeader(args http.Header, reply *struct{}) error { - dest := w.w.Header() - for k := range dest { - if _, ok := args[k]; !ok { - delete(dest, k) - } - } - for k, v := range args { - dest[k] = v - } - return nil -} - -func ServeHTTPResponseWriter(w http.ResponseWriter, conn io.ReadWriteCloser) { - server := rpc.NewServer() - server.Register(&LocalHTTPResponseWriter{ - w: w, - }) - server.ServeConn(conn) -} - -type RemoteHTTPResponseWriter struct { - client *rpc.Client - header http.Header -} - -var _ http.ResponseWriter = (*RemoteHTTPResponseWriter)(nil) - -func (w *RemoteHTTPResponseWriter) Header() http.Header { - if w.header == nil { - w.client.Call("LocalHTTPResponseWriter.Header", struct{}{}, &w.header) - } - return w.header -} - -func (w *RemoteHTTPResponseWriter) Write(b []byte) (int, error) { - if err := w.client.Call("LocalHTTPResponseWriter.SyncHeader", w.header, nil); err != nil { - return 0, err - } - if err := w.client.Call("LocalHTTPResponseWriter.Write", b, nil); err != nil { - return 0, err - } - return len(b), nil -} - -func (w *RemoteHTTPResponseWriter) WriteHeader(statusCode int) { - if err := w.client.Call("LocalHTTPResponseWriter.SyncHeader", w.header, nil); err != nil { - return - } - w.client.Call("LocalHTTPResponseWriter.WriteHeader", statusCode, nil) -} - -func (h *RemoteHTTPResponseWriter) Close() error { - return h.client.Close() -} - -func ConnectHTTPResponseWriter(conn io.ReadWriteCloser) *RemoteHTTPResponseWriter { - return &RemoteHTTPResponseWriter{ - client: rpc.NewClient(conn), - } -} diff --git a/plugin/rpcplugin/http_test.go b/plugin/rpcplugin/http_test.go deleted file mode 100644 index afaaf7756..000000000 --- a/plugin/rpcplugin/http_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package rpcplugin - -import ( - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func testHTTPResponseWriterRPC(w http.ResponseWriter, f func(w http.ResponseWriter)) { - r1, w1 := io.Pipe() - r2, w2 := io.Pipe() - - c1 := NewMuxer(NewReadWriteCloser(r1, w2), false) - defer c1.Close() - - c2 := NewMuxer(NewReadWriteCloser(r2, w1), true) - defer c2.Close() - - id, server := c1.Serve() - go ServeHTTPResponseWriter(w, server) - - remote := ConnectHTTPResponseWriter(c2.Connect(id)) - defer remote.Close() - - f(remote) -} - -func TestHTTP(t *testing.T) { - w := httptest.NewRecorder() - - testHTTPResponseWriterRPC(w, func(w http.ResponseWriter) { - headers := w.Header() - headers.Set("Test-Header-A", "a") - headers.Set("Test-Header-B", "b") - w.Header().Set("Test-Header-C", "c") - w.WriteHeader(http.StatusPaymentRequired) - n, err := w.Write([]byte("this is ")) - assert.Equal(t, 8, n) - assert.NoError(t, err) - n, err = w.Write([]byte("a test")) - assert.Equal(t, 6, n) - assert.NoError(t, err) - }) - - r := w.Result() - defer r.Body.Close() - - assert.Equal(t, http.StatusPaymentRequired, r.StatusCode) - - body, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - assert.EqualValues(t, "this is a test", body) - - assert.Equal(t, "a", r.Header.Get("Test-Header-A")) - assert.Equal(t, "b", r.Header.Get("Test-Header-B")) - assert.Equal(t, "c", r.Header.Get("Test-Header-C")) -} diff --git a/plugin/rpcplugin/io.go b/plugin/rpcplugin/io.go deleted file mode 100644 index 44b89956c..000000000 --- a/plugin/rpcplugin/io.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "bufio" - "encoding/binary" - "io" -) - -type rwc struct { - io.ReadCloser - io.WriteCloser -} - -func (rwc *rwc) Close() (err error) { - err = rwc.WriteCloser.Close() - if rerr := rwc.ReadCloser.Close(); err == nil { - err = rerr - } - return -} - -func NewReadWriteCloser(r io.ReadCloser, w io.WriteCloser) io.ReadWriteCloser { - return &rwc{r, w} -} - -type RemoteIOReader struct { - conn io.ReadWriteCloser -} - -func (r *RemoteIOReader) Read(b []byte) (int, error) { - var buf [10]byte - n := binary.PutVarint(buf[:], int64(len(b))) - if _, err := r.conn.Write(buf[:n]); err != nil { - return 0, err - } - return r.conn.Read(b) -} - -func (r *RemoteIOReader) Close() error { - return r.conn.Close() -} - -func ConnectIOReader(conn io.ReadWriteCloser) io.ReadCloser { - return &RemoteIOReader{conn} -} - -func ServeIOReader(r io.Reader, conn io.ReadWriteCloser) { - cr := bufio.NewReader(conn) - defer conn.Close() - buf := make([]byte, 32*1024) - for { - n, err := binary.ReadVarint(cr) - if err != nil { - break - } - if written, err := io.CopyBuffer(conn, io.LimitReader(r, n), buf); err != nil || written < n { - break - } - } -} diff --git a/plugin/rpcplugin/ipc.go b/plugin/rpcplugin/ipc.go deleted file mode 100644 index e8dd43c04..000000000 --- a/plugin/rpcplugin/ipc.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "io" - "os" -) - -// Returns a new IPC for the parent process and a set of files to pass on to the child. -// -// The returned files must be closed after the child process is started. -func NewIPC() (io.ReadWriteCloser, []*os.File, error) { - parentReader, childWriter, err := os.Pipe() - if err != nil { - return nil, nil, err - } - childReader, parentWriter, err := os.Pipe() - if err != nil { - parentReader.Close() - childWriter.Close() - return nil, nil, err - } - return NewReadWriteCloser(parentReader, parentWriter), []*os.File{childReader, childWriter}, nil -} - -// Returns the IPC instance inherited by the process from its parent. -func InheritedIPC(fd0, fd1 uintptr) (io.ReadWriteCloser, error) { - return NewReadWriteCloser(os.NewFile(fd0, ""), os.NewFile(fd1, "")), nil -} diff --git a/plugin/rpcplugin/ipc_test.go b/plugin/rpcplugin/ipc_test.go deleted file mode 100644 index 76699a11e..000000000 --- a/plugin/rpcplugin/ipc_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package rpcplugin - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" -) - -func TestIPC(t *testing.T) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - pingpong := filepath.Join(dir, "pingpong.exe") - rpcplugintest.CompileGo(t, ` - package main - - import ( - "log" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin" - ) - - func main() { - ipc, err := rpcplugin.InheritedProcessIPC() - if err != nil { - log.Fatal("unable to get inherited ipc") - } - defer ipc.Close() - _, err = ipc.Write([]byte("ping")) - if err != nil { - log.Fatal("unable to write to ipc") - } - b := make([]byte, 10) - n, err := ipc.Read(b) - if err != nil { - log.Fatal("unable to read from ipc") - } - if n != 4 || string(b[:4]) != "pong" { - log.Fatal("unexpected response") - } - } - `, pingpong) - - p, ipc, err := NewProcess(context.Background(), pingpong) - require.NoError(t, err) - defer ipc.Close() - b := make([]byte, 10) - n, err := ipc.Read(b) - require.NoError(t, err) - assert.Equal(t, 4, n) - assert.Equal(t, "ping", string(b[:4])) - _, err = ipc.Write([]byte("pong")) - require.NoError(t, err) - require.NoError(t, p.Wait()) -} diff --git a/plugin/rpcplugin/main.go b/plugin/rpcplugin/main.go deleted file mode 100644 index efb880605..000000000 --- a/plugin/rpcplugin/main.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "bufio" - "encoding/binary" - "fmt" - "log" - "os" -) - -// Makes a set of hooks available via RPC. This function never returns. -func Main(hooks interface{}) { - ipc, err := InheritedProcessIPC() - if err != nil { - log.Fatal(err.Error()) - } - muxer := NewMuxer(ipc, true) - id, conn := muxer.Serve() - buf := make([]byte, 11) - buf[0] = 0 - n := binary.PutVarint(buf[1:], id) - if _, err := muxer.Write(buf[:1+n]); err != nil { - log.Fatal(err.Error()) - } - ServeHooks(hooks, conn, muxer) - os.Exit(0) -} - -// Returns the hooks being served by a call to Main. -func ConnectMain(muxer *Muxer, pluginId string) (*RemoteHooks, error) { - buf := make([]byte, 1) - if _, err := muxer.Read(buf); err != nil { - return nil, err - } else if buf[0] != 0 { - return nil, fmt.Errorf("unexpected control byte") - } - reader := bufio.NewReader(muxer) - id, err := binary.ReadVarint(reader) - if err != nil { - return nil, err - } - - return ConnectHooks(muxer.Connect(id), muxer, pluginId) -} diff --git a/plugin/rpcplugin/main_test.go b/plugin/rpcplugin/main_test.go deleted file mode 100644 index 06423106c..000000000 --- a/plugin/rpcplugin/main_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package rpcplugin - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/mlog" - "github.com/mattermost/mattermost-server/plugin/plugintest" - "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" -) - -func TestMain(t *testing.T) { - // Setup a global logger to catch tests logging outside of app context - // The global logger will be stomped by apps initalizing but that's fine for testing. Ideally this won't happen. - mlog.InitGlobalLogger(mlog.NewLogger(&mlog.LoggerConfiguration{ - EnableConsole: true, - ConsoleJson: true, - ConsoleLevel: "error", - EnableFile: false, - })) - - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - plugin := filepath.Join(dir, "plugin.exe") - rpcplugintest.CompileGo(t, ` - package main - - import ( - "github.com/mattermost/mattermost-server/plugin/rpcplugin" - ) - - type MyPlugin struct {} - - func main() { - rpcplugin.Main(&MyPlugin{}) - } - `, plugin) - - ctx, cancel := context.WithCancel(context.Background()) - p, ipc, err := NewProcess(ctx, plugin) - require.NoError(t, err) - defer p.Wait() - - muxer := NewMuxer(ipc, false) - defer muxer.Close() - - defer cancel() - - var api plugintest.API - - hooks, err := ConnectMain(muxer, "plugin_id") - require.NoError(t, err) - assert.NoError(t, hooks.OnActivate(&api)) - assert.NoError(t, hooks.OnDeactivate()) -} diff --git a/plugin/rpcplugin/muxer.go b/plugin/rpcplugin/muxer.go deleted file mode 100644 index a7260c399..000000000 --- a/plugin/rpcplugin/muxer.go +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "bufio" - "bytes" - "encoding/binary" - "fmt" - "io" - "sync" - "sync/atomic" -) - -// Muxer allows multiple bidirectional streams to be transmitted over a single connection. -// -// Muxer is safe for use by multiple goroutines. -// -// Streams opened on the muxer must be periodically drained in order to reclaim read buffer memory. -// In other words, readers must consume incoming data as it comes in. -type Muxer struct { - // writeMutex guards conn writes - writeMutex sync.Mutex - conn io.ReadWriteCloser - - // didCloseConn is a boolean (0 or 1) used from multiple goroutines via atomic operations - didCloseConn int32 - - // streamsMutex guards streams and nextId - streamsMutex sync.Mutex - nextId int64 - streams map[int64]*muxerStream - - stream0Reader *io.PipeReader - stream0Writer *io.PipeWriter - result chan error -} - -// Creates a new Muxer. -// -// conn must be safe for simultaneous reads by one goroutine and writes by another. -// -// For two muxers communicating with each other via a connection, parity must be true for exactly -// one of them. -func NewMuxer(conn io.ReadWriteCloser, parity bool) *Muxer { - s0r, s0w := io.Pipe() - muxer := &Muxer{ - conn: conn, - streams: make(map[int64]*muxerStream), - result: make(chan error, 1), - nextId: 1, - stream0Reader: s0r, - stream0Writer: s0w, - } - if parity { - muxer.nextId = 2 - } - go muxer.run() - return muxer -} - -// Opens a new stream with a unique id. -// -// Writes made to the stream before the other end calls Connect will be discarded. -func (m *Muxer) Serve() (int64, io.ReadWriteCloser) { - m.streamsMutex.Lock() - id := m.nextId - m.nextId += 2 - m.streamsMutex.Unlock() - return id, m.Connect(id) -} - -// Opens a remotely opened stream. -func (m *Muxer) Connect(id int64) io.ReadWriteCloser { - m.streamsMutex.Lock() - defer m.streamsMutex.Unlock() - mutex := &sync.Mutex{} - stream := &muxerStream{ - id: id, - muxer: m, - mutex: mutex, - readWake: sync.NewCond(mutex), - } - m.streams[id] = stream - return stream -} - -// Calling Read on the muxer directly performs a read on a dedicated, always-open channel. -func (m *Muxer) Read(p []byte) (int, error) { - return m.stream0Reader.Read(p) -} - -// Calling Write on the muxer directly performs a write on a dedicated, always-open channel. -func (m *Muxer) Write(p []byte) (int, error) { - return m.write(p, 0) -} - -// Closes the muxer. -func (m *Muxer) Close() error { - if atomic.CompareAndSwapInt32(&m.didCloseConn, 0, 1) { - m.conn.Close() - } - m.stream0Reader.Close() - m.stream0Writer.Close() - <-m.result - return nil -} - -func (m *Muxer) IsClosed() bool { - return atomic.LoadInt32(&m.didCloseConn) > 0 -} - -func (m *Muxer) write(p []byte, sid int64) (int, error) { - m.writeMutex.Lock() - defer m.writeMutex.Unlock() - if m.IsClosed() { - return 0, fmt.Errorf("muxer closed") - } - var buf [10]byte - n := binary.PutVarint(buf[:], sid) - if _, err := m.conn.Write(buf[:n]); err != nil { - m.shutdown(err) - return 0, err - } - n = binary.PutVarint(buf[:], int64(len(p))) - if _, err := m.conn.Write(buf[:n]); err != nil { - m.shutdown(err) - return 0, err - } - if len(p) > 0 { - if _, err := m.conn.Write(p); err != nil { - m.shutdown(err) - return 0, err - } - } - return len(p), nil -} - -func (m *Muxer) rm(sid int64) { - m.streamsMutex.Lock() - defer m.streamsMutex.Unlock() - delete(m.streams, sid) -} - -func (m *Muxer) run() { - m.shutdown(m.loop()) -} - -func (m *Muxer) loop() error { - reader := bufio.NewReader(m.conn) - - for { - sid, err := binary.ReadVarint(reader) - if err != nil { - return err - } - len, err := binary.ReadVarint(reader) - if err != nil { - return err - } - - if sid == 0 { - if _, err := io.CopyN(m.stream0Writer, reader, len); err != nil { - return err - } - continue - } - - m.streamsMutex.Lock() - stream, ok := m.streams[sid] - m.streamsMutex.Unlock() - if !ok { - if _, err := reader.Discard(int(len)); err != nil { - return err - } - continue - } - - stream.mutex.Lock() - if stream.isClosed { - stream.mutex.Unlock() - if _, err := reader.Discard(int(len)); err != nil { - return err - } - continue - } - if len == 0 { - stream.remoteClosed = true - } else { - _, err = io.CopyN(&stream.readBuf, reader, len) - } - stream.mutex.Unlock() - if err != nil { - return err - } - stream.readWake.Signal() - } -} - -func (m *Muxer) shutdown(err error) { - if atomic.CompareAndSwapInt32(&m.didCloseConn, 0, 1) { - m.conn.Close() - } - go func() { - m.streamsMutex.Lock() - for _, stream := range m.streams { - stream.mutex.Lock() - stream.readWake.Signal() - stream.mutex.Unlock() - } - m.streams = make(map[int64]*muxerStream) - m.streamsMutex.Unlock() - }() - m.result <- err -} - -type muxerStream struct { - id int64 - muxer *Muxer - readBuf bytes.Buffer - mutex *sync.Mutex - readWake *sync.Cond - isClosed bool - remoteClosed bool -} - -func (s *muxerStream) Read(p []byte) (int, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - for { - if s.muxer.IsClosed() { - return 0, fmt.Errorf("muxer closed") - } else if s.isClosed { - return 0, io.EOF - } else if s.readBuf.Len() > 0 { - return s.readBuf.Read(p) - } else if s.remoteClosed { - return 0, io.EOF - } - s.readWake.Wait() - } -} - -func (s *muxerStream) Write(p []byte) (int, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - if s.isClosed { - return 0, fmt.Errorf("stream closed") - } - return s.muxer.write(p, s.id) -} - -func (s *muxerStream) Close() error { - s.mutex.Lock() - defer s.mutex.Unlock() - if !s.isClosed { - s.muxer.write(nil, s.id) - s.isClosed = true - s.muxer.rm(s.id) - } - s.readWake.Signal() - return nil -} diff --git a/plugin/rpcplugin/muxer_test.go b/plugin/rpcplugin/muxer_test.go deleted file mode 100644 index 795a4fb1d..000000000 --- a/plugin/rpcplugin/muxer_test.go +++ /dev/null @@ -1,197 +0,0 @@ -package rpcplugin - -import ( - "io" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMuxer(t *testing.T) { - r1, w1 := io.Pipe() - r2, w2 := io.Pipe() - - alice := NewMuxer(NewReadWriteCloser(r1, w2), false) - defer func() { assert.NoError(t, alice.Close()) }() - - bob := NewMuxer(NewReadWriteCloser(r2, w1), true) - defer func() { assert.NoError(t, bob.Close()) }() - - id1, alice1 := alice.Serve() - defer func() { assert.NoError(t, alice1.Close()) }() - - id2, bob2 := bob.Serve() - defer func() { assert.NoError(t, bob2.Close()) }() - - done1 := make(chan bool) - done2 := make(chan bool) - - go func() { - bob1 := bob.Connect(id1) - defer func() { assert.NoError(t, bob1.Close()) }() - - n, err := bob1.Write([]byte("ping1.0")) - require.NoError(t, err) - assert.Equal(t, n, 7) - - n, err = bob1.Write([]byte("ping1.1")) - require.NoError(t, err) - assert.Equal(t, n, 7) - }() - - go func() { - alice2 := alice.Connect(id2) - defer func() { assert.NoError(t, alice2.Close()) }() - - n, err := alice2.Write([]byte("ping2.0")) - require.NoError(t, err) - assert.Equal(t, n, 7) - - buf := make([]byte, 20) - n, err = alice2.Read(buf) - require.NoError(t, err) - assert.Equal(t, n, 7) - assert.Equal(t, []byte("pong2.0"), buf[:n]) - - done2 <- true - }() - - go func() { - buf := make([]byte, 7) - n, err := io.ReadFull(alice1, buf) - require.NoError(t, err) - assert.Equal(t, n, 7) - assert.Equal(t, []byte("ping1.0"), buf[:n]) - - n, err = alice1.Read(buf) - require.NoError(t, err) - assert.Equal(t, n, 7) - assert.Equal(t, []byte("ping1.1"), buf[:n]) - - done1 <- true - }() - - go func() { - buf := make([]byte, 20) - n, err := bob2.Read(buf) - require.NoError(t, err) - assert.Equal(t, n, 7) - assert.Equal(t, []byte("ping2.0"), buf[:n]) - - n, err = bob2.Write([]byte("pong2.0")) - require.NoError(t, err) - assert.Equal(t, n, 7) - }() - - <-done1 - <-done2 -} - -// Closing a muxer during a read should unblock, but return an error. -func TestMuxer_CloseDuringRead(t *testing.T) { - r1, w1 := io.Pipe() - r2, w2 := io.Pipe() - - alice := NewMuxer(NewReadWriteCloser(r1, w2), false) - - bob := NewMuxer(NewReadWriteCloser(r2, w1), true) - defer func() { assert.NoError(t, bob.Close()) }() - - _, s := alice.Serve() - - go alice.Close() - buf := make([]byte, 20) - n, err := s.Read(buf) - assert.Equal(t, 0, n) - assert.NotNil(t, err) - assert.NotEqual(t, io.EOF, err) -} - -// Closing a stream during a read should unblock and return io.EOF since this is the way to -// gracefully close a connection. -func TestMuxer_StreamCloseDuringRead(t *testing.T) { - r1, w1 := io.Pipe() - r2, w2 := io.Pipe() - - alice := NewMuxer(NewReadWriteCloser(r1, w2), false) - defer func() { assert.NoError(t, alice.Close()) }() - - bob := NewMuxer(NewReadWriteCloser(r2, w1), true) - defer func() { assert.NoError(t, bob.Close()) }() - - _, s := alice.Serve() - - go s.Close() - buf := make([]byte, 20) - n, err := s.Read(buf) - assert.Equal(t, 0, n) - assert.Equal(t, io.EOF, err) -} - -// Closing a stream during a read should unblock and return io.EOF since this is the way for the -// remote to gracefully close a connection. -func TestMuxer_RemoteStreamCloseDuringRead(t *testing.T) { - r1, w1 := io.Pipe() - r2, w2 := io.Pipe() - - alice := NewMuxer(NewReadWriteCloser(r1, w2), false) - defer func() { assert.NoError(t, alice.Close()) }() - - bob := NewMuxer(NewReadWriteCloser(r2, w1), true) - defer func() { assert.NoError(t, bob.Close()) }() - - id, as := alice.Serve() - bs := bob.Connect(id) - - go func() { - as.Write([]byte("foo")) - as.Close() - }() - buf := make([]byte, 20) - n, err := bs.Read(buf) - assert.Equal(t, 3, n) - assert.Equal(t, "foo", string(buf[:n])) - n, err = bs.Read(buf) - assert.Equal(t, 0, n) - assert.Equal(t, io.EOF, err) -} - -// Closing a muxer during a write should unblock, but return an error. -func TestMuxer_CloseDuringWrite(t *testing.T) { - r1, w1 := io.Pipe() - r2, w2 := io.Pipe() - - alice := NewMuxer(NewReadWriteCloser(r1, w2), false) - - // Don't connect bob to let writes will block forever. - defer r2.Close() - defer w1.Close() - - _, s := alice.Serve() - - go alice.Close() - buf := make([]byte, 20) - n, err := s.Write(buf) - assert.Equal(t, 0, n) - assert.NotNil(t, err) - assert.NotEqual(t, io.EOF, err) -} - -func TestMuxer_ReadWrite(t *testing.T) { - r1, w1 := io.Pipe() - r2, w2 := io.Pipe() - - alice := NewMuxer(NewReadWriteCloser(r1, w2), false) - defer func() { assert.NoError(t, alice.Close()) }() - - bob := NewMuxer(NewReadWriteCloser(r2, w1), true) - defer func() { assert.NoError(t, bob.Close()) }() - - go alice.Write([]byte("hello")) - buf := make([]byte, 20) - n, err := bob.Read(buf) - assert.Equal(t, 5, n) - assert.Nil(t, err) - assert.Equal(t, []byte("hello"), buf[:n]) -} diff --git a/plugin/rpcplugin/process.go b/plugin/rpcplugin/process.go deleted file mode 100644 index a795be133..000000000 --- a/plugin/rpcplugin/process.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "context" - "io" -) - -type Process interface { - // Waits for the process to exit and returns an error if a problem occurred or the process exited - // with a non-zero status. - Wait() error -} - -// NewProcess launches an RPC executable in a new process and returns an IPC that can be used to -// communicate with it. -func NewProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) { - return newProcess(ctx, path) -} - -// When called on a process launched with NewProcess, returns the inherited IPC. -func InheritedProcessIPC() (io.ReadWriteCloser, error) { - return inheritedProcessIPC() -} diff --git a/plugin/rpcplugin/process_test.go b/plugin/rpcplugin/process_test.go deleted file mode 100644 index 8d1794293..000000000 --- a/plugin/rpcplugin/process_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package rpcplugin - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" -) - -func TestProcess(t *testing.T) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - ping := filepath.Join(dir, "ping.exe") - rpcplugintest.CompileGo(t, ` - package main - - import ( - "log" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin" - ) - - func main() { - ipc, err := rpcplugin.InheritedProcessIPC() - if err != nil { - log.Fatal("unable to get inherited ipc") - } - defer ipc.Close() - _, err = ipc.Write([]byte("ping")) - if err != nil { - log.Fatal("unable to write to ipc") - } - } - `, ping) - - p, ipc, err := NewProcess(context.Background(), ping) - require.NoError(t, err) - defer ipc.Close() - b := make([]byte, 10) - n, err := ipc.Read(b) - require.NoError(t, err) - assert.Equal(t, 4, n) - assert.Equal(t, "ping", string(b[:4])) - require.NoError(t, p.Wait()) -} - -func TestInvalidProcess(t *testing.T) { - p, ipc, err := NewProcess(context.Background(), "thisfileshouldnotexist") - require.Nil(t, p) - require.Nil(t, ipc) - require.Error(t, err) -} diff --git a/plugin/rpcplugin/process_unix.go b/plugin/rpcplugin/process_unix.go deleted file mode 100644 index 142043cc6..000000000 --- a/plugin/rpcplugin/process_unix.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -// +build !windows - -package rpcplugin - -import ( - "context" - "io" - "os" - "os/exec" -) - -type process struct { - command *exec.Cmd -} - -func newProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) { - ipc, childFiles, err := NewIPC() - if err != nil { - return nil, nil, err - } - defer childFiles[0].Close() - defer childFiles[1].Close() - - cmd := exec.CommandContext(ctx, path) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.ExtraFiles = childFiles - err = cmd.Start() - if err != nil { - ipc.Close() - return nil, nil, err - } - - return &process{ - command: cmd, - }, ipc, nil -} - -func (p *process) Wait() error { - return p.command.Wait() -} - -func inheritedProcessIPC() (io.ReadWriteCloser, error) { - return InheritedIPC(3, 4) -} diff --git a/plugin/rpcplugin/process_windows.go b/plugin/rpcplugin/process_windows.go deleted file mode 100644 index 069f147c1..000000000 --- a/plugin/rpcplugin/process_windows.go +++ /dev/null @@ -1,648 +0,0 @@ -package rpcplugin - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "syscall" - "unicode/utf16" - "unsafe" - - pkgerrors "github.com/pkg/errors" -) - -type process struct { - command *cmd -} - -func newProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) { - ipc, childFiles, err := NewIPC() - if err != nil { - return nil, nil, err - } - defer childFiles[0].Close() - defer childFiles[1].Close() - - cmd := commandContext(ctx, path) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.ExtraFiles = childFiles - cmd.Env = append(os.Environ(), - fmt.Sprintf("MM_IPC_FD0=%v", childFiles[0].Fd()), - fmt.Sprintf("MM_IPC_FD1=%v", childFiles[1].Fd()), - ) - err = cmd.Start() - if err != nil { - ipc.Close() - return nil, nil, err - } - - return &process{ - command: cmd, - }, ipc, nil -} - -func (p *process) Wait() error { - return p.command.Wait() -} - -func inheritedProcessIPC() (io.ReadWriteCloser, error) { - fd0, err := strconv.ParseUint(os.Getenv("MM_IPC_FD0"), 0, 64) - if err != nil { - return nil, pkgerrors.Wrapf(err, "unable to get ipc file descriptor 0") - } - fd1, err := strconv.ParseUint(os.Getenv("MM_IPC_FD1"), 0, 64) - if err != nil { - return nil, pkgerrors.Wrapf(err, "unable to get ipc file descriptor 1") - } - return InheritedIPC(uintptr(fd0), uintptr(fd1)) -} - -// XXX: EVERYTHING BELOW THIS IS COPIED / PASTED STANDARD LIBRARY CODE! -// IT CAN BE DELETED IF / WHEN THIS ISSUE IS RESOLVED: https://github.com/golang/go/issues/21085 - -// Just about all of os/exec/exec.go is copied / pasted below, altered to use our modified startProcess functions even -// further below. - -type cmd struct { - // Path is the path of the command to run. - // - // This is the only field that must be set to a non-zero - // value. If Path is relative, it is evaluated relative - // to Dir. - Path string - - // Args holds command line arguments, including the command as Args[0]. - // If the Args field is empty or nil, Run uses {Path}. - // - // In typical use, both Path and Args are set by calling Command. - Args []string - - // Env specifies the environment of the process. - // If Env is nil, Run uses the current process's environment. - Env []string - - // Dir specifies the working directory of the command. - // If Dir is the empty string, Run runs the command in the - // calling process's current directory. - Dir string - - // Stdin specifies the process's standard input. - // If Stdin is nil, the process reads from the null device (os.DevNull). - // If Stdin is an *os.File, the process's standard input is connected - // directly to that file. - // Otherwise, during the execution of the command a separate - // goroutine reads from Stdin and delivers that data to the command - // over a pipe. In this case, Wait does not complete until the goroutine - // stops copying, either because it has reached the end of Stdin - // (EOF or a read error) or because writing to the pipe returned an error. - Stdin io.Reader - - // Stdout and Stderr specify the process's standard output and error. - // - // If either is nil, Run connects the corresponding file descriptor - // to the null device (os.DevNull). - // - // If Stdout and Stderr are the same writer, at most one - // goroutine at a time will call Write. - Stdout io.Writer - Stderr io.Writer - - // ExtraFiles specifies additional open files to be inherited by the - // new process. It does not include standard input, standard output, or - // standard error. If non-nil, entry i becomes file descriptor 3+i. - // - // BUG(rsc): On OS X 10.6, child processes may sometimes inherit unwanted fds. - // https://golang.org/issue/2603 - ExtraFiles []*os.File - - // SysProcAttr holds optional, operating system-specific attributes. - // Run passes it to os.StartProcess as the os.ProcAttr's Sys field. - SysProcAttr *syscall.SysProcAttr - - // Process is the underlying process, once started. - Process *os.Process - - // ProcessState contains information about an exited process, - // available after a call to Wait or Run. - ProcessState *os.ProcessState - - ctx context.Context // nil means none - lookPathErr error // LookPath error, if any. - finished bool // when Wait was called - childFiles []*os.File - closeAfterStart []io.Closer - closeAfterWait []io.Closer - goroutine []func() error - errch chan error // one send per goroutine - waitDone chan struct{} -} - -func command(name string, arg ...string) *cmd { - cmd := &cmd{ - Path: name, - Args: append([]string{name}, arg...), - } - if filepath.Base(name) == name { - if lp, err := exec.LookPath(name); err != nil { - cmd.lookPathErr = err - } else { - cmd.Path = lp - } - } - return cmd -} - -func commandContext(ctx context.Context, name string, arg ...string) *cmd { - if ctx == nil { - panic("nil Context") - } - cmd := command(name, arg...) - cmd.ctx = ctx - return cmd -} - -func interfaceEqual(a, b interface{}) bool { - defer func() { - recover() - }() - return a == b -} - -func (c *cmd) envv() []string { - if c.Env != nil { - return c.Env - } - return os.Environ() -} - -func (c *cmd) argv() []string { - if len(c.Args) > 0 { - return c.Args - } - return []string{c.Path} -} - -var skipStdinCopyError func(error) bool - -func (c *cmd) stdin() (f *os.File, err error) { - if c.Stdin == nil { - f, err = os.Open(os.DevNull) - if err != nil { - return - } - c.closeAfterStart = append(c.closeAfterStart, f) - return - } - - if f, ok := c.Stdin.(*os.File); ok { - return f, nil - } - - pr, pw, err := os.Pipe() - if err != nil { - return - } - - c.closeAfterStart = append(c.closeAfterStart, pr) - c.closeAfterWait = append(c.closeAfterWait, pw) - c.goroutine = append(c.goroutine, func() error { - _, err := io.Copy(pw, c.Stdin) - if skip := skipStdinCopyError; skip != nil && skip(err) { - err = nil - } - if err1 := pw.Close(); err == nil { - err = err1 - } - return err - }) - return pr, nil -} - -func (c *cmd) stdout() (f *os.File, err error) { - return c.writerDescriptor(c.Stdout) -} - -func (c *cmd) stderr() (f *os.File, err error) { - if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) { - return c.childFiles[1], nil - } - return c.writerDescriptor(c.Stderr) -} - -func (c *cmd) writerDescriptor(w io.Writer) (f *os.File, err error) { - if w == nil { - f, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0) - if err != nil { - return - } - c.closeAfterStart = append(c.closeAfterStart, f) - return - } - - if f, ok := w.(*os.File); ok { - return f, nil - } - - pr, pw, err := os.Pipe() - if err != nil { - return - } - - c.closeAfterStart = append(c.closeAfterStart, pw) - c.closeAfterWait = append(c.closeAfterWait, pr) - c.goroutine = append(c.goroutine, func() error { - _, err := io.Copy(w, pr) - pr.Close() // in case io.Copy stopped due to write error - return err - }) - return pw, nil -} - -func (c *cmd) closeDescriptors(closers []io.Closer) { - for _, fd := range closers { - fd.Close() - } -} - -func lookExtensions(path, dir string) (string, error) { - if filepath.Base(path) == path { - path = filepath.Join(".", path) - } - if dir == "" { - return exec.LookPath(path) - } - if filepath.VolumeName(path) != "" { - return exec.LookPath(path) - } - if len(path) > 1 && os.IsPathSeparator(path[0]) { - return exec.LookPath(path) - } - dirandpath := filepath.Join(dir, path) - // We assume that LookPath will only add file extension. - lp, err := exec.LookPath(dirandpath) - if err != nil { - return "", err - } - ext := strings.TrimPrefix(lp, dirandpath) - return path + ext, nil -} - -// Copied from os/exec/exec.go, altered to use osStartProcess (defined below). -func (c *cmd) Start() error { - if c.lookPathErr != nil { - c.closeDescriptors(c.closeAfterStart) - c.closeDescriptors(c.closeAfterWait) - return c.lookPathErr - } - if runtime.GOOS == "windows" { - lp, err := lookExtensions(c.Path, c.Dir) - if err != nil { - c.closeDescriptors(c.closeAfterStart) - c.closeDescriptors(c.closeAfterWait) - return err - } - c.Path = lp - } - if c.Process != nil { - return errors.New("exec: already started") - } - if c.ctx != nil { - select { - case <-c.ctx.Done(): - c.closeDescriptors(c.closeAfterStart) - c.closeDescriptors(c.closeAfterWait) - return c.ctx.Err() - default: - } - } - - type F func(*cmd) (*os.File, error) - for _, setupFd := range []F{(*cmd).stdin, (*cmd).stdout, (*cmd).stderr} { - fd, err := setupFd(c) - if err != nil { - c.closeDescriptors(c.closeAfterStart) - c.closeDescriptors(c.closeAfterWait) - return err - } - c.childFiles = append(c.childFiles, fd) - } - c.childFiles = append(c.childFiles, c.ExtraFiles...) - - var err error - c.Process, err = osStartProcess(c.Path, c.argv(), &os.ProcAttr{ - Dir: c.Dir, - Files: c.childFiles, - Env: c.envv(), - Sys: c.SysProcAttr, - }) - if err != nil { - c.closeDescriptors(c.closeAfterStart) - c.closeDescriptors(c.closeAfterWait) - return err - } - - c.closeDescriptors(c.closeAfterStart) - - c.errch = make(chan error, len(c.goroutine)) - for _, fn := range c.goroutine { - go func(fn func() error) { - c.errch <- fn() - }(fn) - } - - if c.ctx != nil { - c.waitDone = make(chan struct{}) - go func() { - select { - case <-c.ctx.Done(): - c.Process.Kill() - case <-c.waitDone: - } - }() - } - - return nil -} - -func (c *cmd) Wait() error { - if c.Process == nil { - return errors.New("exec: not started") - } - if c.finished { - return errors.New("exec: Wait was already called") - } - c.finished = true - - state, err := c.Process.Wait() - if c.waitDone != nil { - close(c.waitDone) - } - c.ProcessState = state - - var copyError error - for range c.goroutine { - if err := <-c.errch; err != nil && copyError == nil { - copyError = err - } - } - - c.closeDescriptors(c.closeAfterWait) - - if err != nil { - return err - } else if !state.Success() { - return &exec.ExitError{ProcessState: state} - } - - return copyError -} - -// Copied from os/exec_posix.go, altered to use syscallStartProcess (defined below). -func osStartProcess(name string, argv []string, attr *os.ProcAttr) (p *os.Process, err error) { - // If there is no SysProcAttr (ie. no Chroot or changed - // UID/GID), double-check existence of the directory we want - // to chdir into. We can make the error clearer this way. - if attr != nil && attr.Sys == nil && attr.Dir != "" { - if _, err := os.Stat(attr.Dir); err != nil { - pe := err.(*os.PathError) - pe.Op = "chdir" - return nil, pe - } - } - - sysattr := &syscall.ProcAttr{ - Dir: attr.Dir, - Env: attr.Env, - Sys: attr.Sys, - } - if sysattr.Env == nil { - sysattr.Env = os.Environ() - } - for _, f := range attr.Files { - sysattr.Files = append(sysattr.Files, f.Fd()) - } - - pid, _, e := syscallStartProcess(name, argv, sysattr) - if e != nil { - return nil, &os.PathError{Op: "fork/exec", Path: name, Err: e} - } - return os.FindProcess(pid) -} - -// Everything from this point on is copied from syscall/exec_windows.go - -func makeCmdLine(args []string) string { - var s string - for _, v := range args { - if s != "" { - s += " " - } - s += syscall.EscapeArg(v) - } - return s -} - -func createEnvBlock(envv []string) *uint16 { - if len(envv) == 0 { - return &utf16.Encode([]rune("\x00\x00"))[0] - } - length := 0 - for _, s := range envv { - length += len(s) + 1 - } - length += 1 - - b := make([]byte, length) - i := 0 - for _, s := range envv { - l := len(s) - copy(b[i:i+l], []byte(s)) - copy(b[i+l:i+l+1], []byte{0}) - i = i + l + 1 - } - copy(b[i:i+1], []byte{0}) - - return &utf16.Encode([]rune(string(b)))[0] -} - -func isSlash(c uint8) bool { - return c == '\\' || c == '/' -} - -func normalizeDir(dir string) (name string, err error) { - ndir, err := syscall.FullPath(dir) - if err != nil { - return "", err - } - if len(ndir) > 2 && isSlash(ndir[0]) && isSlash(ndir[1]) { - // dir cannot have \\server\share\path form - return "", syscall.EINVAL - } - return ndir, nil -} - -func volToUpper(ch int) int { - if 'a' <= ch && ch <= 'z' { - ch += 'A' - 'a' - } - return ch -} - -func joinExeDirAndFName(dir, p string) (name string, err error) { - if len(p) == 0 { - return "", syscall.EINVAL - } - if len(p) > 2 && isSlash(p[0]) && isSlash(p[1]) { - // \\server\share\path form - return p, nil - } - if len(p) > 1 && p[1] == ':' { - // has drive letter - if len(p) == 2 { - return "", syscall.EINVAL - } - if isSlash(p[2]) { - return p, nil - } else { - d, err := normalizeDir(dir) - if err != nil { - return "", err - } - if volToUpper(int(p[0])) == volToUpper(int(d[0])) { - return syscall.FullPath(d + "\\" + p[2:]) - } else { - return syscall.FullPath(p) - } - } - } else { - // no drive letter - d, err := normalizeDir(dir) - if err != nil { - return "", err - } - if isSlash(p[0]) { - return syscall.FullPath(d[:2] + p) - } else { - return syscall.FullPath(d + "\\" + p) - } - } -} - -var zeroProcAttr syscall.ProcAttr -var zeroSysProcAttr syscall.SysProcAttr - -// Has minor changes to support file inheritance. -func syscallStartProcess(argv0 string, argv []string, attr *syscall.ProcAttr) (pid int, handle uintptr, err error) { - if len(argv0) == 0 { - return 0, 0, syscall.EWINDOWS - } - if attr == nil { - attr = &zeroProcAttr - } - sys := attr.Sys - if sys == nil { - sys = &zeroSysProcAttr - } - - if len(attr.Files) < 3 { - return 0, 0, syscall.EINVAL - } - - if len(attr.Dir) != 0 { - // StartProcess assumes that argv0 is relative to attr.Dir, - // because it implies Chdir(attr.Dir) before executing argv0. - // Windows CreateProcess assumes the opposite: it looks for - // argv0 relative to the current directory, and, only once the new - // process is started, it does Chdir(attr.Dir). We are adjusting - // for that difference here by making argv0 absolute. - var err error - argv0, err = joinExeDirAndFName(attr.Dir, argv0) - if err != nil { - return 0, 0, err - } - } - argv0p, err := syscall.UTF16PtrFromString(argv0) - if err != nil { - return 0, 0, err - } - - var cmdline string - // Windows CreateProcess takes the command line as a single string: - // use attr.CmdLine if set, else build the command line by escaping - // and joining each argument with spaces - if sys.CmdLine != "" { - cmdline = sys.CmdLine - } else { - cmdline = makeCmdLine(argv) - } - - var argvp *uint16 - if len(cmdline) != 0 { - argvp, err = syscall.UTF16PtrFromString(cmdline) - if err != nil { - return 0, 0, err - } - } - - var dirp *uint16 - if len(attr.Dir) != 0 { - dirp, err = syscall.UTF16PtrFromString(attr.Dir) - if err != nil { - return 0, 0, err - } - } - - // Acquire the fork lock so that no other threads - // create new fds that are not yet close-on-exec - // before we fork. - syscall.ForkLock.Lock() - defer syscall.ForkLock.Unlock() - - p, _ := syscall.GetCurrentProcess() - fd := make([]syscall.Handle, len(attr.Files)) - for i := range attr.Files { - if attr.Files[i] <= 0 { - continue - } - if i < 3 { - err := syscall.DuplicateHandle(p, syscall.Handle(attr.Files[i]), p, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS) - if err != nil { - return 0, 0, err - } - defer syscall.CloseHandle(syscall.Handle(fd[i])) - } else { - // This is the modification that allows files to be inherited. - syscall.SetHandleInformation(syscall.Handle(attr.Files[i]), syscall.HANDLE_FLAG_INHERIT, 1) - defer syscall.SetHandleInformation(syscall.Handle(attr.Files[i]), syscall.HANDLE_FLAG_INHERIT, 0) - } - } - si := new(syscall.StartupInfo) - si.Cb = uint32(unsafe.Sizeof(*si)) - si.Flags = syscall.STARTF_USESTDHANDLES - if sys.HideWindow { - si.Flags |= syscall.STARTF_USESHOWWINDOW - si.ShowWindow = syscall.SW_HIDE - } - si.StdInput = fd[0] - si.StdOutput = fd[1] - si.StdErr = fd[2] - - pi := new(syscall.ProcessInformation) - - flags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT - err = syscall.CreateProcess(argv0p, argvp, nil, nil, true, flags, createEnvBlock(attr.Env), dirp, si, pi) - if err != nil { - return 0, 0, err - } - defer syscall.CloseHandle(syscall.Handle(pi.Thread)) - - return int(pi.ProcessId), uintptr(pi.Process), nil -} diff --git a/plugin/rpcplugin/rpcplugintest/rpcplugintest.go b/plugin/rpcplugin/rpcplugintest/rpcplugintest.go deleted file mode 100644 index 185f741c1..000000000 --- a/plugin/rpcplugin/rpcplugintest/rpcplugintest.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugintest - -import ( - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -func CompileGo(t *testing.T, sourceCode, outputPath string) { - dir, err := ioutil.TempDir(".", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "main.go"), []byte(sourceCode), 0600)) - cmd := exec.Command("go", "build", "-o", outputPath, "main.go") - cmd.Dir = dir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - require.NoError(t, cmd.Run()) -} diff --git a/plugin/rpcplugin/rpcplugintest/supervisor.go b/plugin/rpcplugin/rpcplugintest/supervisor.go deleted file mode 100644 index f3ff847a2..000000000 --- a/plugin/rpcplugin/rpcplugintest/supervisor.go +++ /dev/null @@ -1,312 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugintest - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/plugin/plugintest" -) - -type SupervisorProviderFunc = func(*model.BundleInfo) (plugin.Supervisor, error) - -func TestSupervisorProvider(t *testing.T, sp SupervisorProviderFunc) { - for name, f := range map[string]func(*testing.T, SupervisorProviderFunc){ - "Supervisor": testSupervisor, - "Supervisor_InvalidExecutablePath": testSupervisor_InvalidExecutablePath, - "Supervisor_NonExistentExecutablePath": testSupervisor_NonExistentExecutablePath, - "Supervisor_StartTimeout": testSupervisor_StartTimeout, - // "Supervisor_PluginCrash": testSupervisor_PluginCrash, - // "Supervisor_PluginRepeatedlyCrash": testSupervisor_PluginRepeatedlyCrash, - } { - t.Run(name, func(t *testing.T) { f(t, sp) }) - } -} - -func testSupervisor(t *testing.T, sp SupervisorProviderFunc) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - backend := filepath.Join(dir, "backend.exe") - CompileGo(t, ` - package main - - import ( - "github.com/mattermost/mattermost-server/plugin/rpcplugin" - ) - - type MyPlugin struct {} - - func main() { - rpcplugin.Main(&MyPlugin{}) - } - `, backend) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) - - bundle := model.BundleInfoForPath(dir) - supervisor, err := sp(bundle) - require.NoError(t, err) - require.NoError(t, supervisor.Start(nil)) - require.NoError(t, supervisor.Stop()) -} - -func testSupervisor_InvalidExecutablePath(t *testing.T, sp SupervisorProviderFunc) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "/foo/../../backend.exe"}}`), 0600) - - bundle := model.BundleInfoForPath(dir) - supervisor, err := sp(bundle) - assert.Nil(t, supervisor) - assert.Error(t, err) -} - -func testSupervisor_NonExistentExecutablePath(t *testing.T, sp SupervisorProviderFunc) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "thisfileshouldnotexist"}}`), 0600) - - bundle := model.BundleInfoForPath(dir) - supervisor, err := sp(bundle) - require.NotNil(t, supervisor) - require.NoError(t, err) - - require.Error(t, supervisor.Start(nil)) -} - -// If plugin development goes really wrong, let's make sure plugin activation won't block forever. -func testSupervisor_StartTimeout(t *testing.T, sp SupervisorProviderFunc) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - backend := filepath.Join(dir, "backend.exe") - CompileGo(t, ` - package main - - func main() { - for { - } - } - `, backend) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) - - bundle := model.BundleInfoForPath(dir) - supervisor, err := sp(bundle) - require.NoError(t, err) - require.Error(t, supervisor.Start(nil)) -} - -// Crashed plugins should be relaunched. -func testSupervisor_PluginCrash(t *testing.T, sp SupervisorProviderFunc) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - backend := filepath.Join(dir, "backend.exe") - CompileGo(t, ` - package main - - import ( - "os" - - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/plugin/rpcplugin" - ) - - type Configuration struct { - ShouldExit bool - } - - type MyPlugin struct { - config Configuration - } - - func (p *MyPlugin) OnActivate(api plugin.API) error { - api.LoadPluginConfiguration(&p.config) - return nil - } - - func (p *MyPlugin) OnDeactivate() error { - if p.config.ShouldExit { - os.Exit(1) - } - return nil - } - - func main() { - rpcplugin.Main(&MyPlugin{}) - } - `, backend) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) - - var api plugintest.API - shouldExit := true - api.On("LoadPluginConfiguration", mock.MatchedBy(func(x interface{}) bool { return true })).Return(func(dest interface{}) error { - err := json.Unmarshal([]byte(fmt.Sprintf(`{"ShouldExit": %v}`, shouldExit)), dest) - shouldExit = false - return err - }) - - bundle := model.BundleInfoForPath(dir) - supervisor, err := sp(bundle) - require.NoError(t, err) - - var supervisorWaitErr error - supervisorWaitDone := make(chan bool, 1) - go func() { - supervisorWaitErr = supervisor.Wait() - close(supervisorWaitDone) - }() - - require.NoError(t, supervisor.Start(&api)) - - failed := false - recovered := false - for i := 0; i < 30; i++ { - if supervisor.Hooks().OnDeactivate() == nil { - require.True(t, failed) - recovered = true - break - } else { - failed = true - } - time.Sleep(time.Millisecond * 100) - } - assert.True(t, recovered) - - select { - case <-supervisorWaitDone: - require.Fail(t, "supervisor.Wait() unexpectedly returned") - case <-time.After(500 * time.Millisecond): - } - - require.NoError(t, supervisor.Stop()) - - select { - case <-supervisorWaitDone: - require.Nil(t, supervisorWaitErr) - case <-time.After(5000 * time.Millisecond): - require.Fail(t, "supervisor.Wait() failed to return") - } -} - -// Crashed plugins should be relaunched at most three times. -func testSupervisor_PluginRepeatedlyCrash(t *testing.T, sp SupervisorProviderFunc) { - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - backend := filepath.Join(dir, "backend.exe") - CompileGo(t, ` - package main - - import ( - "net/http" - "os" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin" - ) - - type MyPlugin struct { - crashing bool - } - - func (p *MyPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPost { - p.crashing = true - go func() { - os.Exit(1) - }() - } - - if p.crashing { - w.WriteHeader(http.StatusInternalServerError) - } else { - w.WriteHeader(http.StatusOK) - } - } - - func main() { - rpcplugin.Main(&MyPlugin{}) - } - `, backend) - - ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) - - var api plugintest.API - bundle := model.BundleInfoForPath(dir) - supervisor, err := sp(bundle) - require.NoError(t, err) - - var supervisorWaitErr error - supervisorWaitDone := make(chan bool, 1) - go func() { - supervisorWaitErr = supervisor.Wait() - close(supervisorWaitDone) - }() - - require.NoError(t, supervisor.Start(&api)) - - for attempt := 1; attempt <= 4; attempt++ { - // Verify that the plugin is operational - response := httptest.NewRecorder() - supervisor.Hooks().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/plugins/id", nil)) - require.Equal(t, http.StatusOK, response.Result().StatusCode) - - // Crash the plugin - supervisor.Hooks().ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/plugins/id", nil)) - - // Wait for it to potentially recover - recovered := false - for i := 0; i < 125; i++ { - response := httptest.NewRecorder() - supervisor.Hooks().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/plugins/id", nil)) - if response.Result().StatusCode == http.StatusOK { - recovered = true - break - } - - time.Sleep(time.Millisecond * 100) - } - - if attempt < 4 { - require.Nil(t, supervisorWaitErr) - require.True(t, recovered, "failed to recover after attempt %d", attempt) - } else { - require.False(t, recovered, "unexpectedly recovered after attempt %d", attempt) - } - } - - select { - case <-supervisorWaitDone: - require.NotNil(t, supervisorWaitErr) - case <-time.After(500 * time.Millisecond): - require.Fail(t, "supervisor.Wait() failed to return after plugin crashed") - } - - require.NoError(t, supervisor.Stop()) -} diff --git a/plugin/rpcplugin/sandbox/main_test.go b/plugin/rpcplugin/sandbox/main_test.go deleted file mode 100644 index 4be4a42af..000000000 --- a/plugin/rpcplugin/sandbox/main_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package sandbox - -import ( - "testing" - - "github.com/mattermost/mattermost-server/mlog" -) - -func TestMain(t *testing.T) { - // Setup a global logger to catch tests logging outside of app context - // The global logger will be stomped by apps initalizing but that's fine for testing. Ideally this won't happen. - mlog.InitGlobalLogger(mlog.NewLogger(&mlog.LoggerConfiguration{ - EnableConsole: true, - ConsoleJson: true, - ConsoleLevel: "error", - EnableFile: false, - })) -} diff --git a/plugin/rpcplugin/sandbox/sandbox.go b/plugin/rpcplugin/sandbox/sandbox.go deleted file mode 100644 index 96eff02dd..000000000 --- a/plugin/rpcplugin/sandbox/sandbox.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package sandbox - -import ( - "context" - "io" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin" -) - -type MountPoint struct { - Source string - Destination string - Type string - ReadOnly bool -} - -type Configuration struct { - MountPoints []*MountPoint - WorkingDirectory string -} - -// NewProcess is like rpcplugin.NewProcess, but launches the process in a sandbox. -func NewProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) { - return newProcess(ctx, config, path) -} - -// CheckSupport inspects the platform and environment to determine whether or not there are any -// expected issues with sandboxing. If nil is returned, sandboxing should be used. -func CheckSupport() error { - return checkSupport() -} diff --git a/plugin/rpcplugin/sandbox/sandbox_linux.go b/plugin/rpcplugin/sandbox/sandbox_linux.go deleted file mode 100644 index beb00995d..000000000 --- a/plugin/rpcplugin/sandbox/sandbox_linux.go +++ /dev/null @@ -1,488 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package sandbox - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "syscall" - "unsafe" - - "github.com/pkg/errors" - "golang.org/x/sys/unix" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin" -) - -func init() { - if len(os.Args) < 4 || os.Args[0] != "sandbox.runProcess" { - return - } - - var config Configuration - if err := json.Unmarshal([]byte(os.Args[1]), &config); err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - if err := runProcess(&config, os.Args[2], os.Args[3]); err != nil { - if eerr, ok := err.(*exec.ExitError); ok { - if status, ok := eerr.Sys().(syscall.WaitStatus); ok { - os.Exit(status.ExitStatus()) - } - } - fmt.Println(err.Error()) - os.Exit(1) - } - os.Exit(0) -} - -func systemMountPoints() (points []*MountPoint) { - points = append(points, &MountPoint{ - Source: "proc", - Destination: "/proc", - Type: "proc", - }, &MountPoint{ - Source: "/dev/null", - Destination: "/dev/null", - }, &MountPoint{ - Source: "/dev/zero", - Destination: "/dev/zero", - }, &MountPoint{ - Source: "/dev/full", - Destination: "/dev/full", - }) - - readOnly := []string{ - "/dev/random", - "/dev/urandom", - "/etc/resolv.conf", - "/lib", - "/lib32", - "/lib64", - "/usr/lib", - "/usr/lib32", - "/usr/lib64", - "/etc/ca-certificates", - "/etc/ssl/certs", - "/system/etc/security/cacerts", - "/usr/local/share/certs", - "/etc/pki/tls/certs", - "/etc/openssl/certs", - "/etc/ssl/ca-bundle.pem", - "/etc/pki/tls/cacert.pem", - "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", - } - - for _, v := range []string{"SSL_CERT_FILE", "SSL_CERT_DIR"} { - if path := os.Getenv(v); path != "" { - readOnly = append(readOnly, path) - } - } - - for _, point := range readOnly { - points = append(points, &MountPoint{ - Source: point, - Destination: point, - ReadOnly: true, - }) - } - - return -} - -func runProcess(config *Configuration, path, root string) error { - if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil { - return errors.Wrapf(err, "unable to make root private") - } - - if err := mountMountPoints(root, systemMountPoints()); err != nil { - return errors.Wrapf(err, "unable to mount sandbox system mount points") - } - - if err := mountMountPoints(root, config.MountPoints); err != nil { - return errors.Wrapf(err, "unable to mount sandbox config mount points") - } - - if err := pivotRoot(root); err != nil { - return errors.Wrapf(err, "unable to pivot sandbox root") - } - - if err := os.Mkdir("/tmp", 0755); err != nil { - return errors.Wrapf(err, "unable to create /tmp") - } - - if config.WorkingDirectory != "" { - if err := os.Chdir(config.WorkingDirectory); err != nil { - return errors.Wrapf(err, "unable to set working directory") - } - } - - if err := dropInheritableCapabilities(); err != nil { - return errors.Wrapf(err, "unable to drop inheritable capabilities") - } - - if err := enableSeccompFilter(); err != nil { - return errors.Wrapf(err, "unable to enable seccomp filter") - } - - return runExecutable(path) -} - -func mountMountPoint(root string, mountPoint *MountPoint) error { - isDir := true - if mountPoint.Type == "" { - stat, err := os.Lstat(mountPoint.Source) - if err != nil { - return nil - } - if (stat.Mode() & os.ModeSymlink) != 0 { - if path, err := filepath.EvalSymlinks(mountPoint.Source); err == nil { - newMountPoint := *mountPoint - newMountPoint.Source = path - if err := mountMountPoint(root, &newMountPoint); err != nil { - return errors.Wrapf(err, "unable to mount symbolic link target: "+mountPoint.Source) - } - return nil - } - } - isDir = stat.IsDir() - } - - target := filepath.Join(root, mountPoint.Destination) - - if isDir { - if err := os.MkdirAll(target, 0755); err != nil { - return errors.Wrapf(err, "unable to create directory: "+target) - } - } else { - if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { - return errors.Wrapf(err, "unable to create directory: "+target) - } - f, err := os.Create(target) - if err != nil { - return errors.Wrapf(err, "unable to create file: "+target) - } - f.Close() - } - - flags := uintptr(syscall.MS_NOSUID | syscall.MS_NODEV) - if mountPoint.Type == "" { - flags |= syscall.MS_BIND - } - if mountPoint.ReadOnly { - flags |= syscall.MS_RDONLY - } - - if err := syscall.Mount(mountPoint.Source, target, mountPoint.Type, flags, ""); err != nil { - return errors.Wrapf(err, "unable to mount "+mountPoint.Source) - } - - if (flags & syscall.MS_BIND) != 0 { - // If this was a bind mount, our other flags actually got silently ignored during the above syscall: - // - // If mountflags includes MS_BIND [...] The remaining bits in the mountflags argument are - // also ignored, with the exception of MS_REC. - // - // Furthermore, remounting will fail if we attempt to unset a bit that was inherited from - // the mount's parent: - // - // The mount(2) flags MS_RDONLY, MS_NOSUID, MS_NOEXEC, and the "atime" flags - // (MS_NOATIME, MS_NODIRATIME, MS_RELATIME) settings become locked when propagated from - // a more privileged to a less privileged mount namespace, and may not be changed in the - // less privileged mount namespace. - // - // So we need to get the actual flags, add our new ones, then do a remount if needed. - var stats syscall.Statfs_t - if err := syscall.Statfs(target, &stats); err != nil { - return errors.Wrap(err, "unable to get mount flags for target: "+target) - } - const lockedFlagsMask = unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NOEXEC | unix.MS_NOATIME | unix.MS_NODIRATIME | unix.MS_RELATIME - lockedFlags := uintptr(stats.Flags & lockedFlagsMask) - if lockedFlags != ((flags | lockedFlags) & lockedFlagsMask) { - if err := syscall.Mount("", target, "", flags|lockedFlags|syscall.MS_REMOUNT, ""); err != nil { - return errors.Wrapf(err, "unable to remount "+mountPoint.Source) - } - } - } - - return nil -} - -func mountMountPoints(root string, mountPoints []*MountPoint) error { - for _, mountPoint := range mountPoints { - if err := mountMountPoint(root, mountPoint); err != nil { - return err - } - } - - return nil -} - -func pivotRoot(newRoot string) error { - if err := syscall.Mount(newRoot, newRoot, "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { - return errors.Wrapf(err, "unable to mount new root") - } - - prevRoot := filepath.Join(newRoot, ".prev_root") - - if err := os.MkdirAll(prevRoot, 0700); err != nil { - return errors.Wrapf(err, "unable to create directory for previous root") - } - - if err := syscall.PivotRoot(newRoot, prevRoot); err != nil { - return errors.Wrapf(err, "syscall error") - } - - if err := os.Chdir("/"); err != nil { - return errors.Wrapf(err, "unable to change directory") - } - - prevRoot = "/.prev_root" - - if err := syscall.Unmount(prevRoot, syscall.MNT_DETACH); err != nil { - return errors.Wrapf(err, "unable to unmount previous root") - } - - if err := os.RemoveAll(prevRoot); err != nil { - return errors.Wrapf(err, "unable to remove previous root directory") - } - - return nil -} - -func dropInheritableCapabilities() error { - type capHeader struct { - version uint32 - pid int32 - } - - type capData struct { - effective uint32 - permitted uint32 - inheritable uint32 - } - - var hdr capHeader - var data [2]capData - - if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&hdr)), 0, 0); errno != 0 { - return errors.Wrapf(syscall.Errno(errno), "unable to get capabilities version") - } - - if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&hdr)), uintptr(unsafe.Pointer(&data[0])), 0); errno != 0 { - return errors.Wrapf(syscall.Errno(errno), "unable to get capabilities") - } - - data[0].inheritable = 0 - data[1].inheritable = 0 - if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&hdr)), uintptr(unsafe.Pointer(&data[0])), 0); errno != 0 { - return errors.Wrapf(syscall.Errno(errno), "unable to set inheritable capabilities") - } - - for i := 0; i < 64; i++ { - if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, uintptr(i), 0); errno != 0 && errno != syscall.EINVAL { - return errors.Wrapf(syscall.Errno(errno), "unable to drop bounding set capability") - } - } - - return nil -} - -func enableSeccompFilter() error { - return EnableSeccompFilter(SeccompFilter(NATIVE_AUDIT_ARCH, AllowedSyscalls)) -} - -func runExecutable(path string) error { - childFiles := []*os.File{ - os.NewFile(3, ""), os.NewFile(4, ""), - } - defer childFiles[0].Close() - defer childFiles[1].Close() - - cmd := exec.Command(path) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.ExtraFiles = childFiles - cmd.SysProcAttr = &syscall.SysProcAttr{ - Pdeathsig: syscall.SIGTERM, - } - - if err := cmd.Run(); err != nil { - return err - } - - return nil -} - -type process struct { - command *exec.Cmd - root string -} - -func newProcess(ctx context.Context, config *Configuration, path string) (pOut rpcplugin.Process, rwcOut io.ReadWriteCloser, errOut error) { - configJSON, err := json.Marshal(config) - if err != nil { - return nil, nil, err - } - - ipc, childFiles, err := rpcplugin.NewIPC() - if err != nil { - return nil, nil, err - } - defer childFiles[0].Close() - defer childFiles[1].Close() - - root, err := ioutil.TempDir("", "") - if err != nil { - return nil, nil, err - } - defer func() { - if errOut != nil { - os.RemoveAll(root) - } - }() - - cmd := exec.CommandContext(ctx, "/proc/self/exe") - cmd.Args = []string{"sandbox.runProcess", string(configJSON), path, root} - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.ExtraFiles = childFiles - - cmd.SysProcAttr = &syscall.SysProcAttr{ - Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER, - Pdeathsig: syscall.SIGTERM, - GidMappings: []syscall.SysProcIDMap{ - { - ContainerID: 0, - HostID: os.Getgid(), - Size: 1, - }, - }, - UidMappings: []syscall.SysProcIDMap{ - { - ContainerID: 0, - HostID: os.Getuid(), - Size: 1, - }, - }, - } - - err = cmd.Start() - if err != nil { - ipc.Close() - return nil, nil, err - } - - return &process{ - command: cmd, - root: root, - }, ipc, nil -} - -func (p *process) Wait() error { - defer os.RemoveAll(p.root) - return p.command.Wait() -} - -func init() { - if len(os.Args) < 2 || os.Args[0] != "sandbox.checkSupportInNamespace" { - return - } - - if err := checkSupportInNamespace(os.Args[1]); err != nil { - fmt.Fprintf(os.Stderr, "%v", err.Error()) - os.Exit(1) - } - - os.Exit(0) -} - -func checkSupportInNamespace(root string) error { - if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil { - return errors.Wrapf(err, "unable to make root private") - } - - if err := mountMountPoints(root, systemMountPoints()); err != nil { - return errors.Wrapf(err, "unable to mount sandbox system mount points") - } - - if err := pivotRoot(root); err != nil { - return errors.Wrapf(err, "unable to pivot sandbox root") - } - - if err := dropInheritableCapabilities(); err != nil { - return errors.Wrapf(err, "unable to drop inheritable capabilities") - } - - if err := enableSeccompFilter(); err != nil { - return errors.Wrapf(err, "unable to enable seccomp filter") - } - - if f, err := os.Create(os.DevNull); err != nil { - return errors.Wrapf(err, "unable to open os.DevNull") - } else { - defer f.Close() - if _, err = f.Write([]byte("foo")); err != nil { - return errors.Wrapf(err, "unable to write to os.DevNull") - } - } - - return nil -} - -func checkSupport() error { - if AllowedSyscalls == nil { - return fmt.Errorf("unsupported architecture") - } - - stderr := &bytes.Buffer{} - - root, err := ioutil.TempDir("", "") - if err != nil { - return err - } - defer os.RemoveAll(root) - - cmd := exec.Command("/proc/self/exe") - cmd.Args = []string{"sandbox.checkSupportInNamespace", root} - cmd.Stderr = stderr - cmd.SysProcAttr = &syscall.SysProcAttr{ - Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER, - Pdeathsig: syscall.SIGTERM, - GidMappings: []syscall.SysProcIDMap{ - { - ContainerID: 0, - HostID: os.Getgid(), - Size: 1, - }, - }, - UidMappings: []syscall.SysProcIDMap{ - { - ContainerID: 0, - HostID: os.Getuid(), - Size: 1, - }, - }, - } - - if err := cmd.Start(); err != nil { - return errors.Wrapf(err, "unable to create user namespace") - } - - if err := cmd.Wait(); err != nil { - if _, ok := err.(*exec.ExitError); ok { - return errors.Wrapf(fmt.Errorf("%v", stderr.String()), "unable to prepare namespace") - } - return errors.Wrapf(err, "unable to prepare namespace") - } - - return nil -} diff --git a/plugin/rpcplugin/sandbox/sandbox_linux_test.go b/plugin/rpcplugin/sandbox/sandbox_linux_test.go deleted file mode 100644 index 2bcbf0c57..000000000 --- a/plugin/rpcplugin/sandbox/sandbox_linux_test.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package sandbox - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" -) - -func TestNewProcess(t *testing.T) { - if err := CheckSupport(); err != nil { - t.Skip("sandboxing not supported:", err) - } - - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - ping := filepath.Join(dir, "ping.exe") - rpcplugintest.CompileGo(t, ` - package main - - import ( - "crypto/rand" - "fmt" - "io/ioutil" - "net/http" - "os" - "os/exec" - "syscall" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin" - ) - - var failures int - - type T struct {} - func (T) Errorf(format string, args ...interface{}) { - fmt.Printf(format, args...) - failures++ - } - func (T) FailNow() { - os.Exit(1) - } - - func init() { - if len(os.Args) > 0 && os.Args[0] == "exitImmediately" { - os.Exit(0) - } - } - - func main() { - t := &T{} - - pwd, err := os.Getwd() - assert.NoError(t, err) - assert.Equal(t, "/dir", pwd) - - assert.Equal(t, 0, os.Getgid(), "we should see ourselves as root") - assert.Equal(t, 0, os.Getuid(), "we should see ourselves as root") - - f, err := ioutil.TempFile("", "") - require.NoError(t, err, "we should be able to create temporary files") - f.Close() - - _, err = os.Stat("ping.exe") - assert.NoError(t, err, "we should be able to read files in the working directory") - - buf := make([]byte, 20) - n, err := rand.Read(buf) - assert.Equal(t, 20, n) - assert.NoError(t, err, "we should be able to read from /dev/urandom") - - f, err = os.Create("/dev/zero") - require.NoError(t, err, "we should be able to write to /dev/zero") - defer f.Close() - n, err = f.Write([]byte("foo")) - assert.Equal(t, 3, n) - require.NoError(t, err, "we should be able to write to /dev/zero") - - f, err = os.Create("/dir/foo") - if f != nil { - defer f.Close() - } - assert.Error(t, err, "we shouldn't be able to write to this read-only mount point") - - _, err = ioutil.ReadFile("/etc/resolv.conf") - require.NoError(t, err, "we should be able to read /etc/resolv.conf") - - resp, err := http.Get("https://github.com") - require.NoError(t, err, "we should be able to use the network") - resp.Body.Close() - - status, err := ioutil.ReadFile("/proc/self/status") - require.NoError(t, err, "we should be able to read from /proc") - assert.Regexp(t, status, "CapEff:\\s+0000000000000000", "we should have no effective capabilities") - - require.NoError(t, os.MkdirAll("/tmp/dir2", 0755)) - err = syscall.Mount("/dir", "/tmp/dir2", "", syscall.MS_BIND, "") - assert.Equal(t, syscall.EPERM, err, "we shouldn't be allowed to mount things") - - cmd := exec.Command("/proc/self/exe") - cmd.Args = []string{"exitImmediately"} - cmd.SysProcAttr = &syscall.SysProcAttr{ - Pdeathsig: syscall.SIGTERM, - } - assert.NoError(t, cmd.Run(), "we should be able to re-exec ourself") - - cmd = exec.Command("/proc/self/exe") - cmd.Args = []string{"exitImmediately"} - cmd.SysProcAttr = &syscall.SysProcAttr{ - Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER, - Pdeathsig: syscall.SIGTERM, - } - assert.Error(t, cmd.Run(), "we shouldn't be able to create new namespaces anymore") - - ipc, err := rpcplugin.InheritedProcessIPC() - require.NoError(t, err) - defer ipc.Close() - _, err = ipc.Write([]byte("ping")) - require.NoError(t, err) - - if failures > 0 { - os.Exit(1) - } - } - `, ping) - - p, ipc, err := NewProcess(context.Background(), &Configuration{ - MountPoints: []*MountPoint{ - { - Source: dir, - Destination: "/dir", - ReadOnly: true, - }, - }, - WorkingDirectory: "/dir", - }, "/dir/ping.exe") - require.NoError(t, err) - defer ipc.Close() - b := make([]byte, 10) - n, err := ipc.Read(b) - require.NoError(t, err) - assert.Equal(t, 4, n) - assert.Equal(t, "ping", string(b[:4])) - require.NoError(t, p.Wait()) -} diff --git a/plugin/rpcplugin/sandbox/sandbox_other.go b/plugin/rpcplugin/sandbox/sandbox_other.go deleted file mode 100644 index 3889ecdcc..000000000 --- a/plugin/rpcplugin/sandbox/sandbox_other.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -// +build !linux - -package sandbox - -import ( - "context" - "fmt" - "io" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin" -) - -func newProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) { - return nil, nil, checkSupport() -} - -func checkSupport() error { - return fmt.Errorf("sandboxing is not supported on this platform") -} diff --git a/plugin/rpcplugin/sandbox/sandbox_test.go b/plugin/rpcplugin/sandbox/sandbox_test.go deleted file mode 100644 index e0149e28d..000000000 --- a/plugin/rpcplugin/sandbox/sandbox_test.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package sandbox - -import ( - "testing" -) - -// TestCheckSupport is here for debugging purposes and has no assertions. You can quickly test -// sandboxing support with various systems by compiling the test executable and running this test on -// your target systems. For example, with docker, executed from the root of the repo: -// -// docker run --rm -it -w /go/src/github.com/mattermost/mattermost-server -// -v $(pwd):/go/src/github.com/mattermost/mattermost-server golang:1.9 -// go test -c ./plugin/rpcplugin -// -// docker run --rm -it --privileged -w /opt/mattermost -// -v $(pwd):/opt/mattermost centos:6 -// ./rpcplugin.test --test.v --test.run TestCheckSupport -func TestCheckSupport(t *testing.T) { - if err := CheckSupport(); err != nil { - t.Log(err.Error()) - } -} diff --git a/plugin/rpcplugin/sandbox/seccomp_linux.go b/plugin/rpcplugin/sandbox/seccomp_linux.go deleted file mode 100644 index afe86e90a..000000000 --- a/plugin/rpcplugin/sandbox/seccomp_linux.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package sandbox - -import ( - "syscall" - "unsafe" - - "github.com/pkg/errors" - "golang.org/x/net/bpf" - "golang.org/x/sys/unix" -) - -const ( - SECCOMP_RET_ALLOW = 0x7fff0000 - SECCOMP_RET_ERRNO = 0x00050000 -) - -const ( - EM_X86_64 = 62 - - __AUDIT_ARCH_64BIT = 0x80000000 - __AUDIT_ARCH_LE = 0x40000000 - - AUDIT_ARCH_X86_64 = EM_X86_64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE - - nrSize = 4 - archOffset = nrSize - ipOffset = archOffset + 4 - argsOffset = ipOffset + 8 -) - -type SeccompCondition interface { - Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction -} - -func seccompArgLowWord(arg int, littleEndian bool) uint32 { - offset := uint32(argsOffset + arg*8) - if !littleEndian { - offset += 4 - } - return offset -} - -func seccompArgHighWord(arg int, littleEndian bool) uint32 { - offset := uint32(argsOffset + arg*8) - if littleEndian { - offset += 4 - } - return offset -} - -type SeccompArgHasNoBits struct { - Arg int - Mask uint64 -} - -func (c SeccompArgHasNoBits) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction { - return []bpf.Instruction{ - bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4}, - bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask >> 32), SkipTrue: skipFalseSentinel}, - bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4}, - bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask), SkipTrue: skipFalseSentinel}, - } -} - -type SeccompArgHasAnyBit struct { - Arg int - Mask uint64 -} - -func (c SeccompArgHasAnyBit) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction { - return []bpf.Instruction{ - bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4}, - bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask >> 32), SkipTrue: 2}, - bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4}, - bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask), SkipFalse: skipFalseSentinel}, - } -} - -type SeccompArgEquals struct { - Arg int - Value uint64 -} - -func (c SeccompArgEquals) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction { - return []bpf.Instruction{ - bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4}, - bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(c.Value >> 32), SkipFalse: skipFalseSentinel}, - bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4}, - bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(c.Value), SkipFalse: skipFalseSentinel}, - } -} - -type SeccompConditions struct { - All []SeccompCondition -} - -type SeccompSyscall struct { - Syscall uint32 - Any []SeccompConditions -} - -func SeccompFilter(arch uint32, allowedSyscalls []SeccompSyscall) (filter []bpf.Instruction) { - filter = append(filter, - bpf.LoadAbsolute{Off: archOffset, Size: 4}, - bpf.JumpIf{Cond: bpf.JumpEqual, Val: arch, SkipTrue: 1}, - bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)}, - ) - - filter = append(filter, bpf.LoadAbsolute{Off: 0, Size: nrSize}) - for _, s := range allowedSyscalls { - if s.Any != nil { - syscallStart := len(filter) - filter = append(filter, bpf.Instruction(nil)) - for _, cs := range s.Any { - anyStart := len(filter) - for _, c := range cs.All { - filter = append(filter, c.Filter((arch&__AUDIT_ARCH_LE) != 0, 255)...) - } - filter = append(filter, bpf.RetConstant{Val: SECCOMP_RET_ALLOW}) - for i := anyStart; i < len(filter); i++ { - if jump, ok := filter[i].(bpf.JumpIf); ok { - if len(filter)-i-1 > 255 { - panic("condition too long") - } - if jump.SkipFalse == 255 { - jump.SkipFalse = uint8(len(filter) - i - 1) - } - if jump.SkipTrue == 255 { - jump.SkipTrue = uint8(len(filter) - i - 1) - } - filter[i] = jump - } - } - } - filter = append(filter, bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)}) - if len(filter)-syscallStart-1 > 255 { - panic("conditions too long") - } - filter[syscallStart] = bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(s.Syscall), SkipFalse: uint8(len(filter) - syscallStart - 1)} - } else { - filter = append(filter, - bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(s.Syscall), SkipFalse: 1}, - bpf.RetConstant{Val: SECCOMP_RET_ALLOW}, - ) - } - } - - return append(filter, bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)}) -} - -func EnableSeccompFilter(filter []bpf.Instruction) error { - assembled, err := bpf.Assemble(filter) - if err != nil { - return errors.Wrapf(err, "unable to assemble filter") - } - - sockFilter := make([]unix.SockFilter, len(filter)) - for i, instruction := range assembled { - sockFilter[i].Code = instruction.Op - sockFilter[i].Jt = instruction.Jt - sockFilter[i].Jf = instruction.Jf - sockFilter[i].K = instruction.K - } - - prog := unix.SockFprog{ - Len: uint16(len(sockFilter)), - Filter: &sockFilter[0], - } - - if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, uintptr(unsafe.Pointer(&prog))); errno != 0 { - return errors.Wrapf(syscall.Errno(errno), "syscall error") - } - - return nil -} diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go b/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go deleted file mode 100644 index 7338ebbe0..000000000 --- a/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package sandbox - -import ( - "golang.org/x/sys/unix" -) - -const NATIVE_AUDIT_ARCH = AUDIT_ARCH_X86_64 - -var AllowedSyscalls = []SeccompSyscall{ - {Syscall: unix.SYS_ACCEPT}, - {Syscall: unix.SYS_ACCEPT4}, - {Syscall: unix.SYS_ACCESS}, - {Syscall: unix.SYS_ADJTIMEX}, - {Syscall: unix.SYS_ALARM}, - {Syscall: unix.SYS_ARCH_PRCTL}, - {Syscall: unix.SYS_BIND}, - {Syscall: unix.SYS_BRK}, - {Syscall: unix.SYS_CAPGET}, - {Syscall: unix.SYS_CAPSET}, - {Syscall: unix.SYS_CHDIR}, - {Syscall: unix.SYS_CHMOD}, - {Syscall: unix.SYS_CHOWN}, - {Syscall: unix.SYS_CLOCK_GETRES}, - {Syscall: unix.SYS_CLOCK_GETTIME}, - {Syscall: unix.SYS_CLOCK_NANOSLEEP}, - { - Syscall: unix.SYS_CLONE, - Any: []SeccompConditions{{ - All: []SeccompCondition{SeccompArgHasNoBits{ - Arg: 0, - Mask: unix.CLONE_NEWCGROUP | unix.CLONE_NEWIPC | unix.CLONE_NEWNET | unix.CLONE_NEWNS | unix.CLONE_NEWPID | unix.CLONE_NEWUSER | unix.CLONE_NEWUTS, - }}, - }}, - }, - {Syscall: unix.SYS_CLOSE}, - {Syscall: unix.SYS_CONNECT}, - {Syscall: unix.SYS_COPY_FILE_RANGE}, - {Syscall: unix.SYS_CREAT}, - {Syscall: unix.SYS_DUP}, - {Syscall: unix.SYS_DUP2}, - {Syscall: unix.SYS_DUP3}, - {Syscall: unix.SYS_EPOLL_CREATE}, - {Syscall: unix.SYS_EPOLL_CREATE1}, - {Syscall: unix.SYS_EPOLL_CTL}, - {Syscall: unix.SYS_EPOLL_CTL_OLD}, - {Syscall: unix.SYS_EPOLL_PWAIT}, - {Syscall: unix.SYS_EPOLL_WAIT}, - {Syscall: unix.SYS_EPOLL_WAIT_OLD}, - {Syscall: unix.SYS_EVENTFD}, - {Syscall: unix.SYS_EVENTFD2}, - {Syscall: unix.SYS_EXECVE}, - {Syscall: unix.SYS_EXECVEAT}, - {Syscall: unix.SYS_EXIT}, - {Syscall: unix.SYS_EXIT_GROUP}, - {Syscall: unix.SYS_FACCESSAT}, - {Syscall: unix.SYS_FADVISE64}, - {Syscall: unix.SYS_FALLOCATE}, - {Syscall: unix.SYS_FANOTIFY_MARK}, - {Syscall: unix.SYS_FCHDIR}, - {Syscall: unix.SYS_FCHMOD}, - {Syscall: unix.SYS_FCHMODAT}, - {Syscall: unix.SYS_FCHOWN}, - {Syscall: unix.SYS_FCHOWNAT}, - {Syscall: unix.SYS_FCNTL}, - {Syscall: unix.SYS_FDATASYNC}, - {Syscall: unix.SYS_FGETXATTR}, - {Syscall: unix.SYS_FLISTXATTR}, - {Syscall: unix.SYS_FLOCK}, - {Syscall: unix.SYS_FORK}, - {Syscall: unix.SYS_FREMOVEXATTR}, - {Syscall: unix.SYS_FSETXATTR}, - {Syscall: unix.SYS_FSTAT}, - {Syscall: unix.SYS_FSTATFS}, - {Syscall: unix.SYS_FSYNC}, - {Syscall: unix.SYS_FTRUNCATE}, - {Syscall: unix.SYS_FUTEX}, - {Syscall: unix.SYS_FUTIMESAT}, - {Syscall: unix.SYS_GETCPU}, - {Syscall: unix.SYS_GETCWD}, - {Syscall: unix.SYS_GETDENTS}, - {Syscall: unix.SYS_GETDENTS64}, - {Syscall: unix.SYS_GETEGID}, - {Syscall: unix.SYS_GETEUID}, - {Syscall: unix.SYS_GETGID}, - {Syscall: unix.SYS_GETGROUPS}, - {Syscall: unix.SYS_GETITIMER}, - {Syscall: unix.SYS_GETPEERNAME}, - {Syscall: unix.SYS_GETPGID}, - {Syscall: unix.SYS_GETPGRP}, - {Syscall: unix.SYS_GETPID}, - {Syscall: unix.SYS_GETPPID}, - {Syscall: unix.SYS_GETPRIORITY}, - {Syscall: unix.SYS_GETRANDOM}, - {Syscall: unix.SYS_GETRESGID}, - {Syscall: unix.SYS_GETRESUID}, - {Syscall: unix.SYS_GETRLIMIT}, - {Syscall: unix.SYS_GET_ROBUST_LIST}, - {Syscall: unix.SYS_GETRUSAGE}, - {Syscall: unix.SYS_GETSID}, - {Syscall: unix.SYS_GETSOCKNAME}, - {Syscall: unix.SYS_GETSOCKOPT}, - {Syscall: unix.SYS_GET_THREAD_AREA}, - {Syscall: unix.SYS_GETTID}, - {Syscall: unix.SYS_GETTIMEOFDAY}, - {Syscall: unix.SYS_GETUID}, - {Syscall: unix.SYS_GETXATTR}, - {Syscall: unix.SYS_INOTIFY_ADD_WATCH}, - {Syscall: unix.SYS_INOTIFY_INIT}, - {Syscall: unix.SYS_INOTIFY_INIT1}, - {Syscall: unix.SYS_INOTIFY_RM_WATCH}, - {Syscall: unix.SYS_IO_CANCEL}, - {Syscall: unix.SYS_IOCTL}, - {Syscall: unix.SYS_IO_DESTROY}, - {Syscall: unix.SYS_IO_GETEVENTS}, - {Syscall: unix.SYS_IOPRIO_GET}, - {Syscall: unix.SYS_IOPRIO_SET}, - {Syscall: unix.SYS_IO_SETUP}, - {Syscall: unix.SYS_IO_SUBMIT}, - {Syscall: unix.SYS_KILL}, - {Syscall: unix.SYS_LCHOWN}, - {Syscall: unix.SYS_LGETXATTR}, - {Syscall: unix.SYS_LINK}, - {Syscall: unix.SYS_LINKAT}, - {Syscall: unix.SYS_LISTEN}, - {Syscall: unix.SYS_LISTXATTR}, - {Syscall: unix.SYS_LLISTXATTR}, - {Syscall: unix.SYS_LREMOVEXATTR}, - {Syscall: unix.SYS_LSEEK}, - {Syscall: unix.SYS_LSETXATTR}, - {Syscall: unix.SYS_LSTAT}, - {Syscall: unix.SYS_MADVISE}, - {Syscall: unix.SYS_MEMFD_CREATE}, - {Syscall: unix.SYS_MINCORE}, - {Syscall: unix.SYS_MKDIR}, - {Syscall: unix.SYS_MKDIRAT}, - {Syscall: unix.SYS_MKNOD}, - {Syscall: unix.SYS_MKNODAT}, - {Syscall: unix.SYS_MLOCK}, - {Syscall: unix.SYS_MLOCK2}, - {Syscall: unix.SYS_MLOCKALL}, - {Syscall: unix.SYS_MMAP}, - {Syscall: unix.SYS_MODIFY_LDT}, - {Syscall: unix.SYS_MPROTECT}, - {Syscall: unix.SYS_MQ_GETSETATTR}, - {Syscall: unix.SYS_MQ_NOTIFY}, - {Syscall: unix.SYS_MQ_OPEN}, - {Syscall: unix.SYS_MQ_TIMEDRECEIVE}, - {Syscall: unix.SYS_MQ_TIMEDSEND}, - {Syscall: unix.SYS_MQ_UNLINK}, - {Syscall: unix.SYS_MREMAP}, - {Syscall: unix.SYS_MSGCTL}, - {Syscall: unix.SYS_MSGGET}, - {Syscall: unix.SYS_MSGRCV}, - {Syscall: unix.SYS_MSGSND}, - {Syscall: unix.SYS_MSYNC}, - {Syscall: unix.SYS_MUNLOCK}, - {Syscall: unix.SYS_MUNLOCKALL}, - {Syscall: unix.SYS_MUNMAP}, - {Syscall: unix.SYS_NANOSLEEP}, - {Syscall: unix.SYS_NEWFSTATAT}, - {Syscall: unix.SYS_OPEN}, - {Syscall: unix.SYS_OPENAT}, - {Syscall: unix.SYS_PAUSE}, - { - Syscall: unix.SYS_PERSONALITY, - Any: []SeccompConditions{ - {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0}}}, - {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 8}}}, - {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0x20000}}}, - {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0x20008}}}, - {All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0xffffffff}}}, - }, - }, - {Syscall: unix.SYS_PIPE}, - {Syscall: unix.SYS_PIPE2}, - {Syscall: unix.SYS_POLL}, - {Syscall: unix.SYS_PPOLL}, - {Syscall: unix.SYS_PRCTL}, - {Syscall: unix.SYS_PREAD64}, - {Syscall: unix.SYS_PREADV}, - {Syscall: unix.SYS_PREADV2}, - {Syscall: unix.SYS_PRLIMIT64}, - {Syscall: unix.SYS_PSELECT6}, - {Syscall: unix.SYS_PWRITE64}, - {Syscall: unix.SYS_PWRITEV}, - {Syscall: unix.SYS_PWRITEV2}, - {Syscall: unix.SYS_READ}, - {Syscall: unix.SYS_READAHEAD}, - {Syscall: unix.SYS_READLINK}, - {Syscall: unix.SYS_READLINKAT}, - {Syscall: unix.SYS_READV}, - {Syscall: unix.SYS_RECVFROM}, - {Syscall: unix.SYS_RECVMMSG}, - {Syscall: unix.SYS_RECVMSG}, - {Syscall: unix.SYS_REMAP_FILE_PAGES}, - {Syscall: unix.SYS_REMOVEXATTR}, - {Syscall: unix.SYS_RENAME}, - {Syscall: unix.SYS_RENAMEAT}, - {Syscall: unix.SYS_RENAMEAT2}, - {Syscall: unix.SYS_RESTART_SYSCALL}, - {Syscall: unix.SYS_RMDIR}, - {Syscall: unix.SYS_RT_SIGACTION}, - {Syscall: unix.SYS_RT_SIGPENDING}, - {Syscall: unix.SYS_RT_SIGPROCMASK}, - {Syscall: unix.SYS_RT_SIGQUEUEINFO}, - {Syscall: unix.SYS_RT_SIGRETURN}, - {Syscall: unix.SYS_RT_SIGSUSPEND}, - {Syscall: unix.SYS_RT_SIGTIMEDWAIT}, - {Syscall: unix.SYS_RT_TGSIGQUEUEINFO}, - {Syscall: unix.SYS_SCHED_GETAFFINITY}, - {Syscall: unix.SYS_SCHED_GETATTR}, - {Syscall: unix.SYS_SCHED_GETPARAM}, - {Syscall: unix.SYS_SCHED_GET_PRIORITY_MAX}, - {Syscall: unix.SYS_SCHED_GET_PRIORITY_MIN}, - {Syscall: unix.SYS_SCHED_GETSCHEDULER}, - {Syscall: unix.SYS_SCHED_RR_GET_INTERVAL}, - {Syscall: unix.SYS_SCHED_SETAFFINITY}, - {Syscall: unix.SYS_SCHED_SETATTR}, - {Syscall: unix.SYS_SCHED_SETPARAM}, - {Syscall: unix.SYS_SCHED_SETSCHEDULER}, - {Syscall: unix.SYS_SCHED_YIELD}, - {Syscall: unix.SYS_SECCOMP}, - {Syscall: unix.SYS_SELECT}, - {Syscall: unix.SYS_SEMCTL}, - {Syscall: unix.SYS_SEMGET}, - {Syscall: unix.SYS_SEMOP}, - {Syscall: unix.SYS_SEMTIMEDOP}, - {Syscall: unix.SYS_SENDFILE}, - {Syscall: unix.SYS_SENDMMSG}, - {Syscall: unix.SYS_SENDMSG}, - {Syscall: unix.SYS_SENDTO}, - {Syscall: unix.SYS_SETFSGID}, - {Syscall: unix.SYS_SETFSUID}, - {Syscall: unix.SYS_SETGID}, - {Syscall: unix.SYS_SETGROUPS}, - {Syscall: unix.SYS_SETITIMER}, - {Syscall: unix.SYS_SETPGID}, - {Syscall: unix.SYS_SETPRIORITY}, - {Syscall: unix.SYS_SETREGID}, - {Syscall: unix.SYS_SETRESGID}, - {Syscall: unix.SYS_SETRESUID}, - {Syscall: unix.SYS_SETREUID}, - {Syscall: unix.SYS_SETRLIMIT}, - {Syscall: unix.SYS_SET_ROBUST_LIST}, - {Syscall: unix.SYS_SETSID}, - {Syscall: unix.SYS_SETSOCKOPT}, - {Syscall: unix.SYS_SET_THREAD_AREA}, - {Syscall: unix.SYS_SET_TID_ADDRESS}, - {Syscall: unix.SYS_SETUID}, - {Syscall: unix.SYS_SETXATTR}, - {Syscall: unix.SYS_SHMAT}, - {Syscall: unix.SYS_SHMCTL}, - {Syscall: unix.SYS_SHMDT}, - {Syscall: unix.SYS_SHMGET}, - {Syscall: unix.SYS_SHUTDOWN}, - {Syscall: unix.SYS_SIGALTSTACK}, - {Syscall: unix.SYS_SIGNALFD}, - {Syscall: unix.SYS_SIGNALFD4}, - {Syscall: unix.SYS_SOCKET}, - {Syscall: unix.SYS_SOCKETPAIR}, - {Syscall: unix.SYS_SPLICE}, - {Syscall: unix.SYS_STAT}, - {Syscall: unix.SYS_STATFS}, - {Syscall: unix.SYS_SYMLINK}, - {Syscall: unix.SYS_SYMLINKAT}, - {Syscall: unix.SYS_SYNC}, - {Syscall: unix.SYS_SYNC_FILE_RANGE}, - {Syscall: unix.SYS_SYNCFS}, - {Syscall: unix.SYS_SYSINFO}, - {Syscall: unix.SYS_SYSLOG}, - {Syscall: unix.SYS_TEE}, - {Syscall: unix.SYS_TGKILL}, - {Syscall: unix.SYS_TIME}, - {Syscall: unix.SYS_TIMER_CREATE}, - {Syscall: unix.SYS_TIMER_DELETE}, - {Syscall: unix.SYS_TIMERFD_CREATE}, - {Syscall: unix.SYS_TIMERFD_GETTIME}, - {Syscall: unix.SYS_TIMERFD_SETTIME}, - {Syscall: unix.SYS_TIMER_GETOVERRUN}, - {Syscall: unix.SYS_TIMER_GETTIME}, - {Syscall: unix.SYS_TIMER_SETTIME}, - {Syscall: unix.SYS_TIMES}, - {Syscall: unix.SYS_TKILL}, - {Syscall: unix.SYS_TRUNCATE}, - {Syscall: unix.SYS_UMASK}, - {Syscall: unix.SYS_UNAME}, - {Syscall: unix.SYS_UNLINK}, - {Syscall: unix.SYS_UNLINKAT}, - {Syscall: unix.SYS_UTIME}, - {Syscall: unix.SYS_UTIMENSAT}, - {Syscall: unix.SYS_UTIMES}, - {Syscall: unix.SYS_VFORK}, - {Syscall: unix.SYS_VMSPLICE}, - {Syscall: unix.SYS_WAIT4}, - {Syscall: unix.SYS_WAITID}, - {Syscall: unix.SYS_WRITE}, - {Syscall: unix.SYS_WRITEV}, -} diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_other.go b/plugin/rpcplugin/sandbox/seccomp_linux_other.go deleted file mode 100644 index 5573943cd..000000000 --- a/plugin/rpcplugin/sandbox/seccomp_linux_other.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -// +build linux,!amd64 - -package sandbox - -const NATIVE_AUDIT_ARCH = 0 - -var AllowedSyscalls []SeccompSyscall diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_test.go b/plugin/rpcplugin/sandbox/seccomp_linux_test.go deleted file mode 100644 index 46fe38fe0..000000000 --- a/plugin/rpcplugin/sandbox/seccomp_linux_test.go +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package sandbox - -import ( - "encoding/binary" - "syscall" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/net/bpf" -) - -func seccompData(nr int32, arch uint32, ip uint64, args ...uint64) []byte { - var buf [64]byte - binary.BigEndian.PutUint32(buf[0:], uint32(nr)) - binary.BigEndian.PutUint32(buf[4:], arch) - binary.BigEndian.PutUint64(buf[8:], ip) - for i := 0; i < 6 && i < len(args); i++ { - binary.BigEndian.PutUint64(buf[16+i*8:], args[i]) - } - return buf[:] -} - -func TestSeccompFilter(t *testing.T) { - for name, tc := range map[string]struct { - Filter []bpf.Instruction - Data []byte - Expected bool - }{ - "Allowed": { - Filter: SeccompFilter(0xf00, []SeccompSyscall{ - {Syscall: syscall.SYS_READ}, - {Syscall: syscall.SYS_WRITE}, - }), - Data: seccompData(syscall.SYS_READ, 0xf00, 0), - Expected: true, - }, - "AllFail": { - Filter: SeccompFilter(0xf00, []SeccompSyscall{ - { - Syscall: syscall.SYS_READ, - Any: []SeccompConditions{ - {All: []SeccompCondition{ - &SeccompArgHasAnyBit{Arg: 0, Mask: 2}, - &SeccompArgHasAnyBit{Arg: 1, Mask: 2}, - &SeccompArgHasAnyBit{Arg: 2, Mask: 2}, - &SeccompArgHasAnyBit{Arg: 3, Mask: 2}, - }}, - }, - }, - {Syscall: syscall.SYS_WRITE}, - }), - Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4), - Expected: false, - }, - "AllPass": { - Filter: SeccompFilter(0xf00, []SeccompSyscall{ - { - Syscall: syscall.SYS_READ, - Any: []SeccompConditions{ - {All: []SeccompCondition{ - &SeccompArgHasAnyBit{Arg: 0, Mask: 7}, - &SeccompArgHasAnyBit{Arg: 1, Mask: 7}, - &SeccompArgHasAnyBit{Arg: 2, Mask: 7}, - &SeccompArgHasAnyBit{Arg: 3, Mask: 7}, - }}, - }, - }, - {Syscall: syscall.SYS_WRITE}, - }), - Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4), - Expected: true, - }, - "AnyFail": { - Filter: SeccompFilter(0xf00, []SeccompSyscall{ - { - Syscall: syscall.SYS_READ, - Any: []SeccompConditions{ - {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 0, Mask: 8}}}, - {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 1, Mask: 8}}}, - {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 2, Mask: 8}}}, - {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 3, Mask: 8}}}, - }, - }, - {Syscall: syscall.SYS_WRITE}, - }), - Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4), - Expected: false, - }, - "AnyPass": { - Filter: SeccompFilter(0xf00, []SeccompSyscall{ - { - Syscall: syscall.SYS_READ, - Any: []SeccompConditions{ - {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 0, Mask: 2}}}, - {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 1, Mask: 2}}}, - {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 2, Mask: 2}}}, - {All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 3, Mask: 2}}}, - }, - }, - {Syscall: syscall.SYS_WRITE}, - }), - Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4), - Expected: true, - }, - "BadArch": { - Filter: SeccompFilter(0xf00, []SeccompSyscall{ - {Syscall: syscall.SYS_READ}, - {Syscall: syscall.SYS_WRITE}, - }), - Data: seccompData(syscall.SYS_MOUNT, 0xf01, 0), - Expected: false, - }, - "BadSyscall": { - Filter: SeccompFilter(0xf00, []SeccompSyscall{ - {Syscall: syscall.SYS_READ}, - {Syscall: syscall.SYS_WRITE}, - }), - Data: seccompData(syscall.SYS_MOUNT, 0xf00, 0), - Expected: false, - }, - } { - t.Run(name, func(t *testing.T) { - vm, err := bpf.NewVM(tc.Filter) - require.NoError(t, err) - result, err := vm.Run(tc.Data) - require.NoError(t, err) - if tc.Expected { - assert.Equal(t, SECCOMP_RET_ALLOW, result) - } else { - assert.Equal(t, int(SECCOMP_RET_ERRNO|syscall.EPERM), result) - } - }) - } -} - -func TestSeccompFilter_Conditions(t *testing.T) { - for name, tc := range map[string]struct { - Condition SeccompCondition - Args []uint64 - Expected bool - }{ - "ArgHasAnyBitFail": { - Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x0004}, - Args: []uint64{0x0400008000}, - Expected: false, - }, - "ArgHasAnyBitPass1": { - Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x400000004}, - Args: []uint64{0x8000008004}, - Expected: true, - }, - "ArgHasAnyBitPass2": { - Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x400000004}, - Args: []uint64{0x8400008000}, - Expected: true, - }, - "ArgHasNoBitsFail1": { - Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x1100000011}, - Args: []uint64{0x0000008007}, - Expected: false, - }, - "ArgHasNoBitsFail2": { - Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x1100000011}, - Args: []uint64{0x0700008000}, - Expected: false, - }, - "ArgHasNoBitsPass": { - Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x400000004}, - Args: []uint64{0x8000008000}, - Expected: true, - }, - "ArgEqualsPass": { - Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF}, - Args: []uint64{0x123456789ABCDEF}, - Expected: true, - }, - "ArgEqualsFail1": { - Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF}, - Args: []uint64{0x023456789ABCDEF}, - Expected: false, - }, - "ArgEqualsFail2": { - Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF}, - Args: []uint64{0x123456789ABCDE0}, - Expected: false, - }, - } { - t.Run(name, func(t *testing.T) { - filter := SeccompFilter(0xf00, []SeccompSyscall{ - { - Syscall: 1, - Any: []SeccompConditions{{All: []SeccompCondition{tc.Condition}}}, - }, - }) - vm, err := bpf.NewVM(filter) - require.NoError(t, err) - result, err := vm.Run(seccompData(1, 0xf00, 0, tc.Args...)) - require.NoError(t, err) - if tc.Expected { - assert.Equal(t, SECCOMP_RET_ALLOW, result) - } else { - assert.Equal(t, int(SECCOMP_RET_ERRNO|syscall.EPERM), result) - } - }) - } -} diff --git a/plugin/rpcplugin/sandbox/supervisor.go b/plugin/rpcplugin/sandbox/supervisor.go deleted file mode 100644 index 0e63954fd..000000000 --- a/plugin/rpcplugin/sandbox/supervisor.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package sandbox - -import ( - "context" - "fmt" - "io" - "path/filepath" - "strings" - - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" - "github.com/mattermost/mattermost-server/plugin/rpcplugin" -) - -func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) { - return rpcplugin.SupervisorWithNewProcessFunc(bundle, func(ctx context.Context) (rpcplugin.Process, io.ReadWriteCloser, error) { - executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable)) - if strings.HasPrefix(executable, "..") { - return nil, nil, fmt.Errorf("invalid backend executable") - } - return NewProcess(ctx, &Configuration{ - MountPoints: []*MountPoint{{ - Source: bundle.Path, - Destination: "/plugin", - ReadOnly: true, - }}, - WorkingDirectory: "/plugin", - }, filepath.Join("/plugin", executable)) - }) -} diff --git a/plugin/rpcplugin/sandbox/supervisor_test.go b/plugin/rpcplugin/sandbox/supervisor_test.go deleted file mode 100644 index 245185dd5..000000000 --- a/plugin/rpcplugin/sandbox/supervisor_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package sandbox - -import ( - "testing" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" -) - -func TestSupervisorProvider(t *testing.T) { - if err := CheckSupport(); err != nil { - t.Skip("sandboxing not supported:", err) - } - - rpcplugintest.TestSupervisorProvider(t, SupervisorProvider) -} diff --git a/plugin/rpcplugin/supervisor.go b/plugin/rpcplugin/supervisor.go deleted file mode 100644 index 246747c89..000000000 --- a/plugin/rpcplugin/supervisor.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "context" - "fmt" - "io" - "path/filepath" - "strings" - "sync/atomic" - "time" - - "github.com/mattermost/mattermost-server/mlog" - "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/plugin" -) - -const ( - MaxProcessRestarts = 3 -) - -// Supervisor implements a plugin.Supervisor that launches the plugin in a separate process and -// communicates via RPC. -// -// If the plugin unexpectedly exits, the supervisor will relaunch it after a short delay, but will -// only restart a plugin at most three times. -type Supervisor struct { - hooks atomic.Value - done chan bool - cancel context.CancelFunc - newProcess func(context.Context) (Process, io.ReadWriteCloser, error) - pluginId string - pluginErr error -} - -var _ plugin.Supervisor = (*Supervisor)(nil) - -// Starts the plugin. This method will block until the plugin is successfully launched for the first -// time and will return an error if the plugin cannot be launched at all. -func (s *Supervisor) Start(api plugin.API) error { - ctx, cancel := context.WithCancel(context.Background()) - s.done = make(chan bool, 1) - start := make(chan error, 1) - go s.run(ctx, start, api) - - select { - case <-time.After(time.Second * 3): - cancel() - <-s.done - return fmt.Errorf("timed out waiting for plugin") - case err := <-start: - s.cancel = cancel - return err - } -} - -// Waits for the supervisor to stop (on demand or of its own accord), returning any error that -// triggered the supervisor to stop. -func (s *Supervisor) Wait() error { - <-s.done - return s.pluginErr -} - -// Stops the plugin. -func (s *Supervisor) Stop() error { - s.cancel() - <-s.done - return nil -} - -// Returns the hooks used to communicate with the plugin. The hooks may change if the plugin is -// restarted, so the return value should not be cached. -func (s *Supervisor) Hooks() plugin.Hooks { - return s.hooks.Load().(plugin.Hooks) -} - -func (s *Supervisor) run(ctx context.Context, start chan<- error, api plugin.API) { - defer func() { - close(s.done) - }() - done := ctx.Done() - for i := 0; i <= MaxProcessRestarts; i++ { - s.runPlugin(ctx, start, api) - select { - case <-done: - return - default: - start = nil - if i < MaxProcessRestarts { - mlog.Error("Plugin terminated unexpectedly", mlog.String("plugin_id", s.pluginId)) - time.Sleep(time.Duration((1 + i*i)) * time.Second) - } else { - s.pluginErr = fmt.Errorf("plugin terminated unexpectedly too many times") - mlog.Error("Plugin shutdown", mlog.String("plugin_id", s.pluginId), mlog.Int("max_process_restarts", MaxProcessRestarts), mlog.Err(s.pluginErr)) - } - } - } -} - -func (s *Supervisor) runPlugin(ctx context.Context, start chan<- error, api plugin.API) error { - if start == nil { - mlog.Debug("Restarting plugin", mlog.String("plugin_id", s.pluginId)) - } - - p, ipc, err := s.newProcess(ctx) - if err != nil { - if start != nil { - start <- err - } - return err - } - - muxer := NewMuxer(ipc, false) - closeMuxer := make(chan bool, 1) - muxerClosed := make(chan error, 1) - go func() { - select { - case <-ctx.Done(): - break - case <-closeMuxer: - break - } - muxerClosed <- muxer.Close() - }() - - hooks, err := ConnectMain(muxer, s.pluginId) - if err == nil { - err = hooks.OnActivate(api) - } - - if err != nil { - if start != nil { - start <- err - } - closeMuxer <- true - <-muxerClosed - p.Wait() - return err - } - - s.hooks.Store(hooks) - - if start != nil { - start <- nil - } - p.Wait() - closeMuxer <- true - <-muxerClosed - - return nil -} - -func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) { - return SupervisorWithNewProcessFunc(bundle, func(ctx context.Context) (Process, io.ReadWriteCloser, error) { - executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable)) - if strings.HasPrefix(executable, "..") { - return nil, nil, fmt.Errorf("invalid backend executable") - } - return NewProcess(ctx, filepath.Join(bundle.Path, executable)) - }) -} - -func SupervisorWithNewProcessFunc(bundle *model.BundleInfo, newProcess func(context.Context) (Process, io.ReadWriteCloser, error)) (plugin.Supervisor, error) { - if bundle.Manifest == nil { - return nil, fmt.Errorf("no manifest available") - } else if bundle.Manifest.Backend == nil || bundle.Manifest.Backend.Executable == "" { - return nil, fmt.Errorf("no backend executable specified") - } - executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable)) - if strings.HasPrefix(executable, "..") { - return nil, fmt.Errorf("invalid backend executable") - } - return &Supervisor{pluginId: bundle.Manifest.Id, newProcess: newProcess}, nil -} diff --git a/plugin/rpcplugin/supervisor_test.go b/plugin/rpcplugin/supervisor_test.go deleted file mode 100644 index 06c1fafeb..000000000 --- a/plugin/rpcplugin/supervisor_test.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "testing" - - "github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest" -) - -func TestSupervisorProvider(t *testing.T) { - rpcplugintest.TestSupervisorProvider(t, SupervisorProvider) -} diff --git a/plugin/supervisor.go b/plugin/supervisor.go index f20df7040..0471f7861 100644 --- a/plugin/supervisor.go +++ b/plugin/supervisor.go @@ -1,13 +1,99 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. +// See LICENSE.txt for license information. package plugin -// Supervisor provides the interface for an object that controls the execution of a plugin. This -// type is only relevant to the server, and isn't used by the plugins themselves. -type Supervisor interface { - Start(API) error - Wait() error - Stop() error - Hooks() Hooks +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/hashicorp/go-plugin" + "github.com/mattermost/mattermost-server/mlog" + "github.com/mattermost/mattermost-server/model" +) + +type Supervisor struct { + pluginId string + client *plugin.Client + hooks Hooks + implemented [TotalHooksId]bool +} + +func NewSupervisor(pluginInfo *model.BundleInfo, parentLogger *mlog.Logger, apiImpl API) (*Supervisor, error) { + supervisor := Supervisor{} + + wrappedLogger := pluginInfo.WrapLogger(parentLogger) + + hclogAdaptedLogger := &HclogAdapter{ + wrappedLogger: wrappedLogger, + extrasKey: "wrapped_extras", + } + + pluginMap := map[string]plugin.Plugin{ + "hooks": &HooksPlugin{ + log: wrappedLogger, + apiImpl: apiImpl, + }, + } + + executable := filepath.Clean(filepath.Join(".", pluginInfo.Manifest.Backend.Executable)) + if strings.HasPrefix(executable, "..") { + return nil, fmt.Errorf("invalid backend executable") + } + executable = filepath.Join(pluginInfo.Path, executable) + + supervisor.client = plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: Handshake, + Plugins: pluginMap, + Cmd: exec.Command(executable), + SyncStdout: os.Stdout, + SyncStderr: os.Stdout, + Logger: hclogAdaptedLogger, + StartTimeout: time.Second * 3, + }) + + rpcClient, err := supervisor.client.Client() + if err != nil { + return nil, err + } + + raw, err := rpcClient.Dispense("hooks") + if err != nil { + return nil, err + } + + supervisor.hooks = raw.(Hooks) + + if impl, err := supervisor.hooks.Implemented(); err != nil { + return nil, err + } else { + for _, hookName := range impl { + if hookId, ok := HookNameToId[hookName]; ok { + supervisor.implemented[hookId] = true + } + } + } + + err = supervisor.Hooks().OnActivate() + if err != nil { + return nil, err + } + + return &supervisor, nil +} + +func (sup *Supervisor) Shutdown() { + sup.client.Kill() +} + +func (sup *Supervisor) Hooks() Hooks { + return sup.hooks +} + +func (sup *Supervisor) Implements(hookId int) bool { + return sup.implemented[hookId] } diff --git a/plugin/supervisor_test.go b/plugin/supervisor_test.go new file mode 100644 index 000000000..605835f68 --- /dev/null +++ b/plugin/supervisor_test.go @@ -0,0 +1,148 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package plugin + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/mattermost/mattermost-server/mlog" + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/plugin/plugintest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestSupervisor(t *testing.T) { + for name, f := range map[string]func(*testing.T){ + "Supervisor": testSupervisor, + "Supervisor_InvalidExecutablePath": testSupervisor_InvalidExecutablePath, + "Supervisor_NonExistentExecutablePath": testSupervisor_NonExistentExecutablePath, + "Supervisor_StartTimeout": testSupervisor_StartTimeout, + } { + t.Run(name, f) + } +} + +func CompileGo(t *testing.T, sourceCode, outputPath string) { + dir, err := ioutil.TempDir(".", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "main.go"), []byte(sourceCode), 0600)) + cmd := exec.Command("go", "build", "-o", outputPath, "main.go") + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + require.NoError(t, cmd.Run()) +} + +func testSupervisor(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + backend := filepath.Join(dir, "backend.exe") + CompileGo(t, ` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `, backend) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) + + bundle := model.BundleInfoForPath(dir) + var api plugintest.API + api.On("LoadPluginConfiguration", mock.Anything).Return(nil) + log := mlog.NewLogger(&mlog.LoggerConfiguration{ + EnableConsole: true, + ConsoleJson: true, + ConsoleLevel: "error", + EnableFile: false, + }) + supervisor, err := NewSupervisor(bundle, log, &api) + require.NoError(t, err) + supervisor.Shutdown() +} + +func testSupervisor_InvalidExecutablePath(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "/foo/../../backend.exe"}}`), 0600) + + bundle := model.BundleInfoForPath(dir) + log := mlog.NewLogger(&mlog.LoggerConfiguration{ + EnableConsole: true, + ConsoleJson: true, + ConsoleLevel: "error", + EnableFile: false, + }) + supervisor, err := NewSupervisor(bundle, log, nil) + assert.Nil(t, supervisor) + assert.Error(t, err) +} + +func testSupervisor_NonExistentExecutablePath(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "thisfileshouldnotexist"}}`), 0600) + + bundle := model.BundleInfoForPath(dir) + log := mlog.NewLogger(&mlog.LoggerConfiguration{ + EnableConsole: true, + ConsoleJson: true, + ConsoleLevel: "error", + EnableFile: false, + }) + supervisor, err := NewSupervisor(bundle, log, nil) + require.Error(t, err) + require.Nil(t, supervisor) +} + +// If plugin development goes really wrong, let's make sure plugin activation won't block forever. +func testSupervisor_StartTimeout(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + backend := filepath.Join(dir, "backend.exe") + CompileGo(t, ` + package main + + func main() { + for { + } + } + `, backend) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600) + + bundle := model.BundleInfoForPath(dir) + log := mlog.NewLogger(&mlog.LoggerConfiguration{ + EnableConsole: true, + ConsoleJson: true, + ConsoleLevel: "error", + EnableFile: false, + }) + supervisor, err := NewSupervisor(bundle, log, nil) + require.Error(t, err) + require.Nil(t, supervisor) +} -- cgit v1.2.3-1-g7c22